Browse Source

Merge remote-tracking branch 'origin/master' into webp

# Conflicts:
#	tests/ImageSharp.Tests/TestUtilities/Tests/TestEnvironmentTests.cs
pull/1552/head
Brian Popow 5 years ago
parent
commit
437149a69e
  1. 2
      .editorconfig
  2. 11
      Directory.Build.props
  3. 72
      ImageSharp.sln
  4. 2
      shared-infrastructure
  5. 56
      src/ImageSharp/Common/Helpers/Numerics.cs
  6. 25
      src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
  7. 2
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  8. 13
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  9. 228
      src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs
  10. 10
      src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanLut.cs
  11. 427
      src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
  12. 2
      src/ImageSharp/Formats/Jpeg/Components/Encoder/LuminanceForwardConverter{TPixel}.cs
  13. 148
      src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbToYCbCrConverterLut.cs
  14. 195
      src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbToYCbCrConverterVectorized.cs
  15. 121
      src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter420{TPixel}.cs
  16. 122
      src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter444{TPixel}.cs
  17. 90
      src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter{TPixel}.cs
  18. 463
      src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.cs
  19. 606
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  20. 3
      src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs
  21. 4
      src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs
  22. 58
      src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs
  23. 4
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs
  24. 4
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs
  25. 8
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs
  26. 8
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs
  27. 83
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
  28. 10
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs
  29. 4
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs
  30. 35
      src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs
  31. 124
      src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs
  32. 111
      src/ImageSharp/Formats/Tiff/TiffBitsPerSampleExtensions.cs
  33. 28
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  34. 73
      src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
  35. 7
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  36. 16
      src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
  37. 39
      src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs
  38. 4
      src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs
  39. 38
      src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs
  40. 19
      src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs
  41. 40
      src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs
  42. 2
      src/ImageSharp/ImageSharp.csproj
  43. 10
      src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.cs
  44. 16
      src/ImageSharp/Processing/Extensions/Dithering/DitherExtensions.cs
  45. 136
      src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs
  46. 5
      src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs
  47. 134
      src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
  48. 98
      src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs
  49. 2
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
  50. 12
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs
  51. 44
      src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs
  52. 61
      src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs
  53. 29
      src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs
  54. 4
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs
  55. 4
      tests/Directory.Build.targets
  56. 38
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/BlockOperations/Block8x8F_Scale16X16To8X8.cs
  57. 95
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpeg.cs
  58. 4
      tests/ImageSharp.Benchmarks/Format/Jpeg/Components/Encoder/YCbCrForwardConverterBenchmark.cs
  59. 2
      tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj
  60. 2
      tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj
  61. 73
      tests/ImageSharp.Tests/Common/NumericsTests.cs
  62. 2
      tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
  63. 264
      tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs
  64. 213
      tests/ImageSharp.Tests/Formats/Jpg/RgbToYCbCrConverterTests.cs
  65. 6
      tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColorTests.cs
  66. 11
      tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/PaletteTiffColorTests.cs
  67. 46
      tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColorTests.cs
  68. 48
      tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbTiffColorTests.cs
  69. 6
      tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColorTests.cs
  70. 43
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  71. 41
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
  72. 2
      tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
  73. 2
      tests/ImageSharp.Tests/ImageSharp.Tests.csproj
  74. 13
      tests/ImageSharp.Tests/Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs
  75. 9
      tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs
  76. 4
      tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs
  77. 3
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs
  78. 15
      tests/ImageSharp.Tests/TestImages.cs
  79. 2
      tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs
  80. 17
      tests/ImageSharp.Tests/TestUtilities/Tests/TestEnvironmentTests.cs
  81. 2
      tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithOctreeQuantizer_rgb32.bmp
  82. 2
      tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithWuQuantizer_rgb32.bmp
  83. 4
      tests/Images/External/ReferenceOutput/DitherTests/ApplyDiffusionFilterInBox_Rgba32_CalliphoraPartial.png
  84. 4
      tests/Images/External/ReferenceOutput/DitherTests/ApplyDitherFilterInBox_Rgba32_CalliphoraPartial.png
  85. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_ShouldNotDependOnSinglePixelType_Bgra32_filter0.png
  86. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_ShouldNotDependOnSinglePixelType_Rgb24_filter0.png
  87. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_ShouldNotDependOnSinglePixelType_Rgba32_filter0.png
  88. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_ShouldNotDependOnSinglePixelType_RgbaVector_filter0.png
  89. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Atkinson.png
  90. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Burks.png
  91. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_FloydSteinberg.png
  92. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_JarvisJudiceNinke.png
  93. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Sierra2.png
  94. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Sierra3.png
  95. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_SierraLite.png
  96. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_StevensonArce.png
  97. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Stucki.png
  98. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Atkinson.png
  99. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Burks.png
  100. 4
      tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_FloydSteinberg.png

2
.editorconfig

@ -75,7 +75,7 @@ indent_style = tab
[*.{cs,csx,cake,vb,vbx}]
# Default Severity for all .NET Code Style rules below
dotnet_analyzer_diagnostic.severity = warning
dotnet_analyzer_diagnostic.category-style.severity = warning
##########################################
# Language Rules

11
Directory.Build.props

@ -18,13 +18,12 @@
<!-- Import the shared global .props file -->
<Import Project="$(MSBuildThisFileDirectory)shared-infrastructure\msbuild\props\SixLabors.Global.props" />
<!-- Custom Project Configuration doesn't seem to copy this setting properly. -->
<PropertyGroup Condition="$(Configuration.StartsWith('Debug')) == true">
<Optimize>false</Optimize>
</PropertyGroup>
<!--
Ensure all custom build configurations based upon "Release" are optimized.
This is easier than setting each project individually.
-->
<PropertyGroup Condition="$(Configuration.StartsWith('Release')) == true">
<Optimize>true</Optimize>
</PropertyGroup>
</Project>

72
ImageSharp.sln

@ -621,115 +621,43 @@ Global
EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Debug-InnerLoop|Any CPU = Debug-InnerLoop|Any CPU
Debug-InnerLoop|x64 = Debug-InnerLoop|x64
Debug-InnerLoop|x86 = Debug-InnerLoop|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
Release-InnerLoop|Any CPU = Release-InnerLoop|Any CPU
Release-InnerLoop|x64 = Release-InnerLoop|x64
Release-InnerLoop|x86 = Release-InnerLoop|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Debug|x64.ActiveCfg = Debug|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Debug|x64.Build.0 = Debug|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Debug|x86.ActiveCfg = Debug|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Debug|x86.Build.0 = Debug|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Debug-InnerLoop|Any CPU.ActiveCfg = Debug-InnerLoop|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Debug-InnerLoop|Any CPU.Build.0 = Debug-InnerLoop|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Debug-InnerLoop|x64.ActiveCfg = Debug-InnerLoop|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Debug-InnerLoop|x64.Build.0 = Debug-InnerLoop|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Debug-InnerLoop|x86.ActiveCfg = Debug-InnerLoop|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Debug-InnerLoop|x86.Build.0 = Debug-InnerLoop|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Release|Any CPU.Build.0 = Release|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Release|x64.ActiveCfg = Release|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Release|x64.Build.0 = Release|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Release|x86.ActiveCfg = Release|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Release|x86.Build.0 = Release|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Release-InnerLoop|Any CPU.ActiveCfg = Release-InnerLoop|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Release-InnerLoop|Any CPU.Build.0 = Release-InnerLoop|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Release-InnerLoop|x64.ActiveCfg = Release-InnerLoop|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Release-InnerLoop|x64.Build.0 = Release-InnerLoop|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Release-InnerLoop|x86.ActiveCfg = Release-InnerLoop|Any CPU
{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Release-InnerLoop|x86.Build.0 = Release-InnerLoop|Any CPU
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|x64.ActiveCfg = Debug|Any CPU
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|x64.Build.0 = Debug|Any CPU
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|x86.ActiveCfg = Debug|Any CPU
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|x86.Build.0 = Debug|Any CPU
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug-InnerLoop|Any CPU.ActiveCfg = Debug-InnerLoop|Any CPU
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug-InnerLoop|Any CPU.Build.0 = Debug-InnerLoop|Any CPU
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug-InnerLoop|x64.ActiveCfg = Debug-InnerLoop|x64
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug-InnerLoop|x64.Build.0 = Debug-InnerLoop|x64
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug-InnerLoop|x86.ActiveCfg = Debug-InnerLoop|x86
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug-InnerLoop|x86.Build.0 = Debug-InnerLoop|x86
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.Build.0 = Release|Any CPU
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|x64.ActiveCfg = Release|Any CPU
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|x64.Build.0 = Release|Any CPU
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|x86.ActiveCfg = Release|Any CPU
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|x86.Build.0 = Release|Any CPU
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release-InnerLoop|Any CPU.ActiveCfg = Release-InnerLoop|Any CPU
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release-InnerLoop|Any CPU.Build.0 = Release-InnerLoop|Any CPU
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release-InnerLoop|x64.ActiveCfg = Release-InnerLoop|x64
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release-InnerLoop|x64.Build.0 = Release-InnerLoop|x64
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release-InnerLoop|x86.ActiveCfg = Release-InnerLoop|x86
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release-InnerLoop|x86.Build.0 = Release-InnerLoop|x86
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug|x64.ActiveCfg = Debug|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug|x64.Build.0 = Debug|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug|x86.ActiveCfg = Debug|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug|x86.Build.0 = Debug|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug-InnerLoop|Any CPU.ActiveCfg = Debug-InnerLoop|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug-InnerLoop|Any CPU.Build.0 = Debug-InnerLoop|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug-InnerLoop|x64.ActiveCfg = Debug-InnerLoop|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug-InnerLoop|x64.Build.0 = Debug-InnerLoop|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug-InnerLoop|x86.ActiveCfg = Debug-InnerLoop|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug-InnerLoop|x86.Build.0 = Debug-InnerLoop|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Release|Any CPU.Build.0 = Release|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Release|x64.ActiveCfg = Release|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Release|x64.Build.0 = Release|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Release|x86.ActiveCfg = Release|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Release|x86.Build.0 = Release|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Release-InnerLoop|Any CPU.ActiveCfg = Release-InnerLoop|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Release-InnerLoop|Any CPU.Build.0 = Release-InnerLoop|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Release-InnerLoop|x64.ActiveCfg = Release-InnerLoop|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Release-InnerLoop|x64.Build.0 = Release-InnerLoop|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Release-InnerLoop|x86.ActiveCfg = Release-InnerLoop|Any CPU
{2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Release-InnerLoop|x86.Build.0 = Release-InnerLoop|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Debug|x64.ActiveCfg = Debug|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Debug|x64.Build.0 = Debug|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Debug|x86.ActiveCfg = Debug|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Debug|x86.Build.0 = Debug|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Debug-InnerLoop|Any CPU.ActiveCfg = Debug-InnerLoop|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Debug-InnerLoop|Any CPU.Build.0 = Debug-InnerLoop|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Debug-InnerLoop|x64.ActiveCfg = Debug-InnerLoop|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Debug-InnerLoop|x64.Build.0 = Debug-InnerLoop|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Debug-InnerLoop|x86.ActiveCfg = Debug-InnerLoop|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Debug-InnerLoop|x86.Build.0 = Debug-InnerLoop|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Release|Any CPU.Build.0 = Release|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Release|x64.ActiveCfg = Release|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Release|x64.Build.0 = Release|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Release|x86.ActiveCfg = Release|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Release|x86.Build.0 = Release|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Release-InnerLoop|Any CPU.ActiveCfg = Release-InnerLoop|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Release-InnerLoop|Any CPU.Build.0 = Release-InnerLoop|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Release-InnerLoop|x64.ActiveCfg = Release-InnerLoop|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Release-InnerLoop|x64.Build.0 = Release-InnerLoop|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Release-InnerLoop|x86.ActiveCfg = Release-InnerLoop|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Release-InnerLoop|x86.Build.0 = Release-InnerLoop|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

2
shared-infrastructure

@ -1 +1 @@
Subproject commit 48e73f455f15eafefbe3175efc7433e5f277e506
Subproject commit 9b94ebc4be9b7a8d7620c257e6ee485455973332

56
src/ImageSharp/Common/Helpers/Numerics.cs

@ -23,6 +23,16 @@ namespace SixLabors.ImageSharp
private const int ShuffleAlphaControl = 0b_11_11_11_11;
#endif
#if !SUPPORTS_BITOPERATIONS
private static ReadOnlySpan<byte> Log2DeBruijn => new byte[32]
{
00, 09, 01, 10, 13, 21, 02, 29,
11, 14, 16, 18, 22, 25, 03, 30,
08, 12, 20, 28, 15, 17, 24, 07,
19, 27, 23, 06, 26, 05, 04, 31
};
#endif
/// <summary>
/// Determine the Greatest CommonDivisor (GCD) of two numbers.
/// </summary>
@ -756,7 +766,7 @@ namespace SixLabors.ImageSharp
/// widening them to 32-bit integers and performing four additions.
/// </summary>
/// <remarks>
/// <code>byte(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)</code>
/// <c>byte(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)</c>
/// is widened and added onto <paramref name="accumulator"/> as such:
/// <code>
/// accumulator += i32(1, 2, 3, 4);
@ -825,5 +835,49 @@ namespace SixLabors.ImageSharp
return Sse2.ConvertToInt32(vsum);
}
#endif
/// <summary>
/// Calculates floored log of the specified value, base 2.
/// Note that by convention, input value 0 returns 0 since Log(0) is undefined.
/// </summary>
/// <param name="value">The value.</param>
public static int Log2(uint value)
{
#if SUPPORTS_BITOPERATIONS
return BitOperations.Log2(value);
#else
return Log2SoftwareFallback(value);
#endif
}
#if !SUPPORTS_BITOPERATIONS
/// <summary>
/// Calculates floored log of the specified value, base 2.
/// Note that by convention, input value 0 returns 0 since Log(0) is undefined.
/// Bit hacking with deBruijn sequence, extremely fast yet does not use any intrinsics so will work on every platform/runtime.
/// </summary>
/// <remarks>
/// Description of this bit hacking can be found here:
/// https://cstheory.stackexchange.com/questions/19524/using-the-de-bruijn-sequence-to-find-the-lceil-log-2-v-rceil-of-an-integer
/// </remarks>
/// <param name="value">The value.</param>
private static int Log2SoftwareFallback(uint value)
{
// No AggressiveInlining due to large method size
// Has conventional contract 0->0 (Log(0) is undefined) by default, no need for if checking
// Fill trailing zeros with ones, eg 00010010 becomes 00011111
value |= value >> 01;
value |= value >> 02;
value |= value >> 04;
value |= value >> 08;
value |= value >> 16;
// uint.MaxValue >> 27 is always in range [0 - 31] so we use Unsafe.AddByteOffset to avoid bounds check
return Unsafe.AddByteOffset(
ref MemoryMarshal.GetReference(Log2DeBruijn),
(IntPtr)(int)((value * 0x07C4ACDDu) >> 27)); // uint|long -> IntPtr cast on 32-bit platforms does expensive overflow checks not needed here
}
#endif
}
}

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

@ -532,6 +532,7 @@ namespace SixLabors.ImageSharp
/// <summary>
/// Performs a multiplication and an addition of the <see cref="Vector256{T}"/>.
/// </summary>
/// <remarks>ret = (vm0 * vm1) + va</remarks>
/// <param name="va">The vector to add to the intermediate result.</param>
/// <param name="vm0">The first vector to multiply.</param>
/// <param name="vm1">The second vector to multiply.</param>
@ -552,6 +553,30 @@ namespace SixLabors.ImageSharp
}
}
/// <summary>
/// Performs a multiplication and a substraction of the <see cref="Vector256{T}"/>.
/// </summary>
/// <remarks>ret = (vm0 * vm1) - vs</remarks>
/// <param name="vs">The vector to substract from the intermediate result.</param>
/// <param name="vm0">The first vector to multiply.</param>
/// <param name="vm1">The second vector to multiply.</param>
/// <returns>The <see cref="Vector256{T}"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static Vector256<float> MultiplySubstract(
in Vector256<float> vs,
in Vector256<float> vm0,
in Vector256<float> vm1)
{
if (Fma.IsSupported)
{
return Fma.MultiplySubtract(vm1, vm0, vs);
}
else
{
return Avx.Subtract(Avx.Multiply(vm0, vm1), vs);
}
}
/// <summary>
/// <see cref="ByteToNormalizedFloat"/> as many elements as possible, slicing them down (keeping the remainder).
/// </summary>

2
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -98,7 +98,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = options.BitsPerPixel;
this.writeV4Header = options.SupportTransparency;
this.quantizer = options.Quantizer ?? KnownQuantizers.Wu;
this.quantizer = options.Quantizer ?? KnownQuantizers.Octree;
}
/// <summary>

13
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -151,7 +151,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
// since the palette is unchanging. This allows a reduction of memory usage across
// multi frame gifs using a global palette.
EuclideanPixelMap<TPixel> pixelMap = default;
bool pixelMapSet = false;
bool pixelMapHasValue = false;
for (int i = 0; i < image.Frames.Count; i++)
{
ImageFrame<TPixel> frame = image.Frames[i];
@ -166,17 +166,22 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
else
{
if (!pixelMapSet)
if (!pixelMapHasValue)
{
pixelMapSet = true;
pixelMapHasValue = true;
pixelMap = new EuclideanPixelMap<TPixel>(this.configuration, quantized.Palette);
}
using var paletteFrameQuantizer = new PaletteQuantizer<TPixel>(this.configuration, this.quantizer.Options, pixelMap);
using var paletteFrameQuantizer = new PaletteQuantizer<TPixel>(this.configuration, this.quantizer.Options, pixelMap, true);
using IndexedImageFrame<TPixel> paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds());
this.WriteImageData(paletteQuantized, stream);
}
}
if (pixelMapHasValue)
{
pixelMap.Dispose();
}
}
private void EncodeLocal<TPixel>(Image<TPixel> image, IndexedImageFrame<TPixel> quantized, Stream stream)

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

@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
/// <summary>
/// Represents a Jpeg block with <see cref="float"/> coefficients.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
[StructLayout(LayoutKind.Explicit)]
internal partial struct Block8x8F : IEquatable<Block8x8F>
{
/// <summary>
@ -27,29 +27,69 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
public const int Size = 64;
#pragma warning disable SA1600 // ElementsMustBeDocumented
[FieldOffset(0)]
public Vector4 V0L;
[FieldOffset(16)]
public Vector4 V0R;
[FieldOffset(32)]
public Vector4 V1L;
[FieldOffset(48)]
public Vector4 V1R;
[FieldOffset(64)]
public Vector4 V2L;
[FieldOffset(80)]
public Vector4 V2R;
[FieldOffset(96)]
public Vector4 V3L;
[FieldOffset(112)]
public Vector4 V3R;
[FieldOffset(128)]
public Vector4 V4L;
[FieldOffset(144)]
public Vector4 V4R;
[FieldOffset(160)]
public Vector4 V5L;
[FieldOffset(176)]
public Vector4 V5R;
[FieldOffset(192)]
public Vector4 V6L;
[FieldOffset(208)]
public Vector4 V6R;
[FieldOffset(224)]
public Vector4 V7L;
[FieldOffset(240)]
public Vector4 V7R;
#if SUPPORTS_RUNTIME_INTRINSICS
/// <summary>
/// A number of rows of 8 scalar coefficients each in <see cref="Block8x8F"/>
/// </summary>
public const int RowCount = 8;
[FieldOffset(0)]
public Vector256<float> V0;
[FieldOffset(32)]
public Vector256<float> V1;
[FieldOffset(64)]
public Vector256<float> V2;
[FieldOffset(96)]
public Vector256<float> V3;
[FieldOffset(128)]
public Vector256<float> V4;
[FieldOffset(160)]
public Vector256<float> V5;
[FieldOffset(192)]
public Vector256<float> V6;
[FieldOffset(224)]
public Vector256<float> V7;
#endif
#pragma warning restore SA1600 // ElementsMustBeDocumented
/// <summary>
@ -278,14 +318,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
if (Avx.IsSupported)
{
var valueVec = Vector256.Create(value);
Unsafe.As<Vector4, Vector256<float>>(ref this.V0L) = Avx.Multiply(Unsafe.As<Vector4, Vector256<float>>(ref this.V0L), valueVec);
Unsafe.As<Vector4, Vector256<float>>(ref this.V1L) = Avx.Multiply(Unsafe.As<Vector4, Vector256<float>>(ref this.V1L), valueVec);
Unsafe.As<Vector4, Vector256<float>>(ref this.V2L) = Avx.Multiply(Unsafe.As<Vector4, Vector256<float>>(ref this.V2L), valueVec);
Unsafe.As<Vector4, Vector256<float>>(ref this.V3L) = Avx.Multiply(Unsafe.As<Vector4, Vector256<float>>(ref this.V3L), valueVec);
Unsafe.As<Vector4, Vector256<float>>(ref this.V4L) = Avx.Multiply(Unsafe.As<Vector4, Vector256<float>>(ref this.V4L), valueVec);
Unsafe.As<Vector4, Vector256<float>>(ref this.V5L) = Avx.Multiply(Unsafe.As<Vector4, Vector256<float>>(ref this.V5L), valueVec);
Unsafe.As<Vector4, Vector256<float>>(ref this.V6L) = Avx.Multiply(Unsafe.As<Vector4, Vector256<float>>(ref this.V6L), valueVec);
Unsafe.As<Vector4, Vector256<float>>(ref this.V7L) = Avx.Multiply(Unsafe.As<Vector4, Vector256<float>>(ref this.V7L), valueVec);
this.V0 = Avx.Multiply(this.V0, valueVec);
this.V1 = Avx.Multiply(this.V1, valueVec);
this.V2 = Avx.Multiply(this.V2, valueVec);
this.V3 = Avx.Multiply(this.V3, valueVec);
this.V4 = Avx.Multiply(this.V4, valueVec);
this.V5 = Avx.Multiply(this.V5, valueVec);
this.V6 = Avx.Multiply(this.V6, valueVec);
this.V7 = Avx.Multiply(this.V7, valueVec);
}
else
#endif
@ -319,45 +359,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
#if SUPPORTS_RUNTIME_INTRINSICS
if (Avx.IsSupported)
{
Unsafe.As<Vector4, Vector256<float>>(ref this.V0L)
= Avx.Multiply(
Unsafe.As<Vector4, Vector256<float>>(ref this.V0L),
Unsafe.As<Vector4, Vector256<float>>(ref other.V0L));
Unsafe.As<Vector4, Vector256<float>>(ref this.V1L)
= Avx.Multiply(
Unsafe.As<Vector4, Vector256<float>>(ref this.V1L),
Unsafe.As<Vector4, Vector256<float>>(ref other.V1L));
Unsafe.As<Vector4, Vector256<float>>(ref this.V2L)
= Avx.Multiply(
Unsafe.As<Vector4, Vector256<float>>(ref this.V2L),
Unsafe.As<Vector4, Vector256<float>>(ref other.V2L));
Unsafe.As<Vector4, Vector256<float>>(ref this.V3L)
= Avx.Multiply(
Unsafe.As<Vector4, Vector256<float>>(ref this.V3L),
Unsafe.As<Vector4, Vector256<float>>(ref other.V3L));
Unsafe.As<Vector4, Vector256<float>>(ref this.V4L)
= Avx.Multiply(
Unsafe.As<Vector4, Vector256<float>>(ref this.V4L),
Unsafe.As<Vector4, Vector256<float>>(ref other.V4L));
Unsafe.As<Vector4, Vector256<float>>(ref this.V5L)
= Avx.Multiply(
Unsafe.As<Vector4, Vector256<float>>(ref this.V5L),
Unsafe.As<Vector4, Vector256<float>>(ref other.V5L));
Unsafe.As<Vector4, Vector256<float>>(ref this.V6L)
= Avx.Multiply(
Unsafe.As<Vector4, Vector256<float>>(ref this.V6L),
Unsafe.As<Vector4, Vector256<float>>(ref other.V6L));
Unsafe.As<Vector4, Vector256<float>>(ref this.V7L)
= Avx.Multiply(
Unsafe.As<Vector4, Vector256<float>>(ref this.V7L),
Unsafe.As<Vector4, Vector256<float>>(ref other.V7L));
this.V0 = Avx.Multiply(this.V0, other.V0);
this.V1 = Avx.Multiply(this.V1, other.V1);
this.V2 = Avx.Multiply(this.V2, other.V2);
this.V3 = Avx.Multiply(this.V3, other.V3);
this.V4 = Avx.Multiply(this.V4, other.V4);
this.V5 = Avx.Multiply(this.V5, other.V5);
this.V6 = Avx.Multiply(this.V6, other.V6);
this.V7 = Avx.Multiply(this.V7, other.V7);
}
else
#endif
@ -392,14 +401,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
if (Avx.IsSupported)
{
var valueVec = Vector256.Create(value);
Unsafe.As<Vector4, Vector256<float>>(ref this.V0L) = Avx.Add(Unsafe.As<Vector4, Vector256<float>>(ref this.V0L), valueVec);
Unsafe.As<Vector4, Vector256<float>>(ref this.V1L) = Avx.Add(Unsafe.As<Vector4, Vector256<float>>(ref this.V1L), valueVec);
Unsafe.As<Vector4, Vector256<float>>(ref this.V2L) = Avx.Add(Unsafe.As<Vector4, Vector256<float>>(ref this.V2L), valueVec);
Unsafe.As<Vector4, Vector256<float>>(ref this.V3L) = Avx.Add(Unsafe.As<Vector4, Vector256<float>>(ref this.V3L), valueVec);
Unsafe.As<Vector4, Vector256<float>>(ref this.V4L) = Avx.Add(Unsafe.As<Vector4, Vector256<float>>(ref this.V4L), valueVec);
Unsafe.As<Vector4, Vector256<float>>(ref this.V5L) = Avx.Add(Unsafe.As<Vector4, Vector256<float>>(ref this.V5L), valueVec);
Unsafe.As<Vector4, Vector256<float>>(ref this.V6L) = Avx.Add(Unsafe.As<Vector4, Vector256<float>>(ref this.V6L), valueVec);
Unsafe.As<Vector4, Vector256<float>>(ref this.V7L) = Avx.Add(Unsafe.As<Vector4, Vector256<float>>(ref this.V7L), valueVec);
this.V0 = Avx.Add(this.V0, valueVec);
this.V1 = Avx.Add(this.V1, valueVec);
this.V2 = Avx.Add(this.V2, valueVec);
this.V3 = Avx.Add(this.V3, valueVec);
this.V4 = Avx.Add(this.V4, valueVec);
this.V5 = Avx.Add(this.V5, valueVec);
this.V6 = Avx.Add(this.V6, valueVec);
this.V7 = Avx.Add(this.V7, valueVec);
}
else
#endif
@ -468,81 +477,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
DivideRoundAll(ref dest, ref qt);
}
/// <summary>
/// Scales the 16x16 region represented by the 4 source blocks to the 8x8 DST block.
/// </summary>
/// <param name="destination">The destination block.</param>
/// <param name="source">The source block.</param>
public static unsafe void Scale16X16To8X8(ref Block8x8F destination, ReadOnlySpan<Block8x8F> source)
{
#if SUPPORTS_RUNTIME_INTRINSICS
if (Avx2.IsSupported)
{
Scale16X16To8X8Vectorized(ref destination, source);
return;
}
#endif
Scale16X16To8X8Scalar(ref destination, source);
}
private static void Scale16X16To8X8Vectorized(ref Block8x8F destination, ReadOnlySpan<Block8x8F> source)
{
#if SUPPORTS_RUNTIME_INTRINSICS
Debug.Assert(Avx2.IsSupported, "AVX2 is required to execute this method");
var f2 = Vector256.Create(2f);
var f025 = Vector256.Create(0.25f);
Vector256<int> switchInnerDoubleWords = Unsafe.As<byte, Vector256<int>>(ref MemoryMarshal.GetReference(SimdUtils.HwIntrinsics.PermuteMaskSwitchInnerDWords8x32));
ref Vector256<float> destRef = ref Unsafe.As<Block8x8F, Vector256<float>>(ref destination);
for (int i = 0; i < 2; i++)
{
ref Vector256<float> in1 = ref Unsafe.As<Block8x8F, Vector256<float>>(ref Unsafe.Add(ref MemoryMarshal.GetReference(source), 2 * i));
ref Vector256<float> in2 = ref Unsafe.As<Block8x8F, Vector256<float>>(ref Unsafe.Add(ref MemoryMarshal.GetReference(source), (2 * i) + 1));
for (int j = 0; j < 8; j += 2)
{
Vector256<float> a = Unsafe.Add(ref in1, j);
Vector256<float> b = Unsafe.Add(ref in1, j + 1);
Vector256<float> c = Unsafe.Add(ref in2, j);
Vector256<float> d = Unsafe.Add(ref in2, j + 1);
Vector256<float> calc1 = Avx.Shuffle(a, c, 0b10_00_10_00);
Vector256<float> calc2 = Avx.Shuffle(a, c, 0b11_01_11_01);
Vector256<float> calc3 = Avx.Shuffle(b, d, 0b10_00_10_00);
Vector256<float> calc4 = Avx.Shuffle(b, d, 0b11_01_11_01);
Vector256<float> sum = Avx.Add(Avx.Add(calc1, calc2), Avx.Add(calc3, calc4));
Vector256<float> add = Avx.Add(sum, f2);
Vector256<float> res = Avx.Multiply(add, f025);
destRef = Avx2.PermuteVar8x32(res, switchInnerDoubleWords);
destRef = ref Unsafe.Add(ref destRef, 1);
}
}
#endif
}
private static unsafe void Scale16X16To8X8Scalar(ref Block8x8F destination, ReadOnlySpan<Block8x8F> source)
{
for (int i = 0; i < 4; i++)
{
int dstOff = ((i & 2) << 4) | ((i & 1) << 2);
Block8x8F iSource = source[i];
for (int y = 0; y < 4; y++)
{
for (int x = 0; x < 4; x++)
{
int j = (16 * y) + (2 * x);
float sum = iSource[j] + iSource[j + 1] + iSource[j + 8] + iSource[j + 9];
destination[(8 * y) + x + dstOff] = (sum + 2) * .25F;
}
}
}
}
[MethodImpl(InliningOptions.ShortMethod)]
private static void DivideRoundAll(ref Block8x8F a, ref Block8x8F b)
{
@ -553,19 +487,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
var vadd = Vector256.Create(.5F);
var vone = Vector256.Create(1f);
ref Vector256<float> aBase = ref Unsafe.AsRef(Unsafe.As<Vector4, Vector256<float>>(ref a.V0L));
ref Vector256<float> bBase = ref Unsafe.AsRef(Unsafe.As<Vector4, Vector256<float>>(ref b.V0L));
ref Vector256<float> aEnd = ref Unsafe.Add(ref aBase, 8);
do
for (int i = 0; i < RowCount; i++)
{
Vector256<float> voff = Avx.Multiply(Avx.Min(Avx.Max(vnegOne, aBase), vone), vadd);
Unsafe.Add(ref aBase, 0) = Avx.Add(Avx.Divide(aBase, bBase), voff);
aBase = ref Unsafe.Add(ref aBase, 1);
bBase = ref Unsafe.Add(ref bBase, 1);
ref Vector256<float> aRow = ref Unsafe.Add(ref a.V0, i);
ref Vector256<float> bRow = ref Unsafe.Add(ref b.V0, i);
Vector256<float> voff = Avx.Multiply(Avx.Min(Avx.Max(vnegOne, aRow), vone), vadd);
aRow = Avx.Add(Avx.Divide(aRow, bRow), voff);
}
while (Unsafe.IsAddressLessThan(ref aBase, ref aEnd));
}
else
#endif
@ -805,26 +733,26 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
Vector256<float> t0 = Avx.UnpackLow(r0, r1);
Vector256<float> t2 = Avx.UnpackLow(r2, r3);
Vector256<float> v = Avx.Shuffle(t0, t2, 0x4E);
Unsafe.As<Vector4, Vector256<float>>(ref d.V0L) = Avx.Blend(t0, v, 0xCC);
Unsafe.As<Vector4, Vector256<float>>(ref d.V1L) = Avx.Blend(t2, v, 0x33);
d.V0 = Avx.Blend(t0, v, 0xCC);
d.V1 = Avx.Blend(t2, v, 0x33);
Vector256<float> t4 = Avx.UnpackLow(r4, r5);
Vector256<float> t6 = Avx.UnpackLow(r6, r7);
v = Avx.Shuffle(t4, t6, 0x4E);
Unsafe.As<Vector4, Vector256<float>>(ref d.V4L) = Avx.Blend(t4, v, 0xCC);
Unsafe.As<Vector4, Vector256<float>>(ref d.V5L) = Avx.Blend(t6, v, 0x33);
d.V4 = Avx.Blend(t4, v, 0xCC);
d.V5 = Avx.Blend(t6, v, 0x33);
Vector256<float> t1 = Avx.UnpackHigh(r0, r1);
Vector256<float> t3 = Avx.UnpackHigh(r2, r3);
v = Avx.Shuffle(t1, t3, 0x4E);
Unsafe.As<Vector4, Vector256<float>>(ref d.V2L) = Avx.Blend(t1, v, 0xCC);
Unsafe.As<Vector4, Vector256<float>>(ref d.V3L) = Avx.Blend(t3, v, 0x33);
d.V2 = Avx.Blend(t1, v, 0xCC);
d.V3 = Avx.Blend(t3, v, 0x33);
Vector256<float> t5 = Avx.UnpackHigh(r4, r5);
Vector256<float> t7 = Avx.UnpackHigh(r6, r7);
v = Avx.Shuffle(t5, t7, 0x4E);
Unsafe.As<Vector4, Vector256<float>>(ref d.V6L) = Avx.Blend(t5, v, 0xCC);
Unsafe.As<Vector4, Vector256<float>>(ref d.V7L) = Avx.Blend(t7, v, 0x33);
d.V6 = Avx.Blend(t5, v, 0xCC);
d.V7 = Avx.Blend(t7, v, 0x33);
}
else
#endif

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

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
@ -44,7 +44,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
}
}
this.Values = new uint[maxValue + 1];
this.Values = new int[maxValue + 1];
int code = 0;
int k = 0;
@ -54,7 +54,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
int bits = (i + 1) << 24;
for (int j = 0; j < spec.Count[i]; j++)
{
this.Values[spec.Values[k]] = (uint)(bits | code);
this.Values[spec.Values[k]] = bits | code;
code++;
k++;
}
@ -66,6 +66,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
/// <summary>
/// Gets the collection of huffman values.
/// </summary>
public uint[] Values { get; }
public int[] Values { get; }
}
}
}

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

@ -0,0 +1,427 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
internal class HuffmanScanEncoder
{
/// <summary>
/// Number of bytes cached before being written to target stream via Stream.Write(byte[], offest, count).
/// </summary>
/// <remarks>
/// This is subject to change, 1024 seems to be the best value in terms of performance.
/// <see cref="Emit(int, int)"/> expects it to be at least 8 (see comments in method body).
/// </remarks>
private const int EmitBufferSizeInBytes = 1024;
/// <summary>
/// A buffer for reducing the number of stream writes when emitting Huffman tables.
/// </summary>
private readonly byte[] emitBuffer = new byte[EmitBufferSizeInBytes];
/// <summary>
/// Number of filled bytes in <see cref="emitBuffer"/> buffer
/// </summary>
private int emitLen = 0;
/// <summary>
/// Emmited bits 'micro buffer' before being transfered to the <see cref="emitBuffer"/>.
/// </summary>
private int accumulatedBits;
/// <summary>
/// Number of jagged bits stored in <see cref="accumulatedBits"/>
/// </summary>
private int bitCount;
private Block8x8F temporalBlock1;
private Block8x8F temporalBlock2;
/// <summary>
/// The output stream. All attempted writes after the first error become no-ops.
/// </summary>
private readonly Stream target;
public HuffmanScanEncoder(Stream outputStream)
{
this.target = outputStream;
}
/// <summary>
/// Encodes the image with no subsampling.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The pixel accessor providing access to the image pixels.</param>
/// <param name="luminanceQuantTable">Luminance quantization table provided by the callee</param>
/// <param name="chrominanceQuantTable">Chrominance quantization table provided by the callee</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param>
public void Encode444<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
var unzig = ZigZag.CreateUnzigTable();
// 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 YCbCrForwardConverter444<TPixel>(frame);
for (int y = 0; y < pixels.Height; y += 8)
{
cancellationToken.ThrowIfCancellationRequested();
currentRows.Update(pixelBuffer, y);
for (int x = 0; x < pixels.Width; x += 8)
{
pixelConverter.Convert(x, y, ref currentRows);
prevDCY = this.WriteBlock(
QuantIndex.Luminance,
prevDCY,
ref pixelConverter.Y,
ref luminanceQuantTable,
ref unzig);
prevDCCb = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCb,
ref pixelConverter.Cb,
ref chrominanceQuantTable,
ref unzig);
prevDCCr = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCr,
ref pixelConverter.Cr,
ref chrominanceQuantTable,
ref unzig);
}
}
this.FlushInternalBuffer();
}
/// <summary>
/// Encodes the image with subsampling. The Cb and Cr components are each subsampled
/// at a factor of 2 both horizontally and vertically.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The pixel accessor providing access to the image pixels.</param>
/// <param name="luminanceQuantTable">Luminance quantization table provided by the callee</param>
/// <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>
{
var unzig = ZigZag.CreateUnzigTable();
// 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);
for (int y = 0; y < pixels.Height; y += 16)
{
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(
QuantIndex.Luminance,
prevDCY,
ref pixelConverter.YLeft,
ref luminanceQuantTable,
ref unzig);
prevDCY = this.WriteBlock(
QuantIndex.Luminance,
prevDCY,
ref pixelConverter.YRight,
ref luminanceQuantTable,
ref unzig);
}
prevDCCb = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCb,
ref pixelConverter.Cb,
ref chrominanceQuantTable,
ref unzig);
prevDCCr = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCr,
ref pixelConverter.Cr,
ref chrominanceQuantTable,
ref unzig);
}
}
this.FlushInternalBuffer();
}
/// <summary>
/// Encodes the image with no chroma, just luminance.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The pixel accessor providing access to the image pixels.</param>
/// <param name="luminanceQuantTable">Luminance quantization table provided by the callee</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param>
public void EncodeGrayscale<TPixel>(Image<TPixel> pixels, ref Block8x8F luminanceQuantTable, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
var unzig = ZigZag.CreateUnzigTable();
// ReSharper disable once InconsistentNaming
int prevDCY = 0;
var pixelConverter = LuminanceForwardConverter<TPixel>.Create();
ImageFrame<TPixel> frame = pixels.Frames.RootFrame;
Buffer2D<TPixel> pixelBuffer = frame.PixelBuffer;
RowOctet<TPixel> currentRows = default;
for (int y = 0; y < pixels.Height; y += 8)
{
cancellationToken.ThrowIfCancellationRequested();
currentRows.Update(pixelBuffer, y);
for (int x = 0; x < pixels.Width; x += 8)
{
pixelConverter.Convert(frame, x, y, ref currentRows);
prevDCY = this.WriteBlock(
QuantIndex.Luminance,
prevDCY,
ref pixelConverter.Y,
ref luminanceQuantTable,
ref unzig);
}
}
this.FlushInternalBuffer();
}
/// <summary>
/// Writes a block of pixel data using the given quantization table,
/// returning the post-quantized DC value of the DCT-transformed block.
/// The block is in natural (not zig-zag) order.
/// </summary>
/// <param name="index">The quantization table index.</param>
/// <param name="prevDC">The previous DC value.</param>
/// <param name="src">Source block</param>
/// <param name="quant">Quantization table</param>
/// <param name="unZig">The 8x8 Unzig block.</param>
/// <returns>The <see cref="int"/>.</returns>
private int WriteBlock(
QuantIndex index,
int prevDC,
ref Block8x8F src,
ref Block8x8F quant,
ref ZigZag unZig)
{
ref Block8x8F refTemp1 = ref this.temporalBlock1;
ref Block8x8F refTemp2 = ref this.temporalBlock2;
FastFloatingPointDCT.TransformFDCT(ref src, ref refTemp1, ref refTemp2);
Block8x8F.Quantize(ref refTemp1, ref refTemp2, ref quant, ref unZig);
int dc = (int)refTemp2[0];
// Emit the DC delta.
this.EmitHuffRLE((2 * (int)index) + 0, 0, dc - prevDC);
// Emit the AC components.
int h = (2 * (int)index) + 1;
int runLength = 0;
for (int zig = 1; zig < Block8x8F.Size; zig++)
{
int ac = (int)refTemp2[zig];
if (ac == 0)
{
runLength++;
}
else
{
while (runLength > 15)
{
this.EmitHuff(h, 0xf0);
runLength -= 16;
}
this.EmitHuffRLE(h, runLength, ac);
runLength = 0;
}
}
if (runLength > 0)
{
this.EmitHuff(h, 0x00);
}
return dc;
}
/// <summary>
/// Emits the least significant count of bits to the stream write buffer.
/// The precondition is bits
/// <example>
/// &lt; 1&lt;&lt;nBits &amp;&amp; nBits &lt;= 16
/// </example>
/// .
/// </summary>
/// <param name="bits">The packed bits.</param>
/// <param name="count">The number of bits</param>
[MethodImpl(InliningOptions.ShortMethod)]
private void Emit(int bits, int count)
{
count += this.bitCount;
bits <<= 32 - count;
bits |= this.accumulatedBits;
// Only write if more than 8 bits.
if (count >= 8)
{
// Track length
while (count >= 8)
{
byte b = (byte)(bits >> 24);
this.emitBuffer[this.emitLen++] = b;
if (b == byte.MaxValue)
{
this.emitBuffer[this.emitLen++] = byte.MinValue;
}
bits <<= 8;
count -= 8;
}
// This can emit 4 times of:
// 1 byte guaranteed
// 1 extra byte.MinValue byte if previous one was byte.MaxValue
// Thus writing (1 + 1) * 4 = 8 bytes max
// So we must check if emit buffer has extra 8 bytes, if not - call stream.Write
if (this.emitLen > EmitBufferSizeInBytes - 8)
{
this.target.Write(this.emitBuffer, 0, this.emitLen);
this.emitLen = 0;
}
}
this.accumulatedBits = bits;
this.bitCount = count;
}
/// <summary>
/// Emits the given value with the given Huffman encoder.
/// </summary>
/// <param name="index">The index of the Huffman encoder</param>
/// <param name="value">The value to encode.</param>
[MethodImpl(InliningOptions.ShortMethod)]
private void EmitHuff(int index, int value)
{
int x = HuffmanLut.TheHuffmanLut[index].Values[value];
this.Emit(x & ((1 << 24) - 1), x >> 24);
}
/// <summary>
/// Emits a run of runLength copies of value encoded with the given Huffman encoder.
/// </summary>
/// <param name="index">The index of the Huffman encoder</param>
/// <param name="runLength">The number of copies to encode.</param>
/// <param name="value">The value to encode.</param>
[MethodImpl(InliningOptions.ShortMethod)]
private void EmitHuffRLE(int index, int runLength, int value)
{
int a = value;
int b = value;
if (a < 0)
{
a = -value;
b = value - 1;
}
int bt = GetHuffmanEncodingLength((uint)a);
this.EmitHuff(index, (runLength << 4) | bt);
if (bt > 0)
{
this.Emit(b & ((1 << bt) - 1), bt);
}
}
/// <summary>
/// Writes remaining bytes from internal buffer to the target stream.
/// </summary>
/// <remarks>Pads last byte with 1's if necessary</remarks>
private void FlushInternalBuffer()
{
// pad last byte with 1's
int padBitsCount = 8 - (this.bitCount % 8);
if (padBitsCount != 0)
{
this.Emit((1 << padBitsCount) - 1, padBitsCount);
}
// flush remaining bytes
if (this.emitLen != 0)
{
this.target.Write(this.emitBuffer, 0, this.emitLen);
}
}
/// <summary>
/// Calculates how many minimum bits needed to store given value for Huffman jpeg encoding.
/// </summary>
/// <remarks>
/// This method returns 0 for input value 0. This is done specificaly for huffman encoding
/// </remarks>
/// <param name="value">The value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetHuffmanEncodingLength(uint value)
{
DebugGuard.IsTrue(value <= (1 << 16), "Huffman encoder is supposed to encode a value of 16bit size max");
#if SUPPORTS_BITOPERATIONS
// This should have been implemented as (BitOperations.Log2(value) + 1) as in non-intrinsic implementation
// But internal log2 is implementated like this: (31 - (int)Lzcnt.LeadingZeroCount(value))
// BitOperations.Log2 implementation also checks if input value is zero for the convention 0->0
// Lzcnt would return 32 for input value of 0 - no need to check that with branching
// Fallback code if Lzcnt is not supported still use if-check
// But most modern CPUs support this instruction so this should not be a problem
return 32 - System.Numerics.BitOperations.LeadingZeroCount(value);
#else
// Ideally:
// if 0 - return 0 in this case
// else - return log2(value) + 1
//
// Hack based on input value constaint:
// We know that input values are guaranteed to be maximum 16 bit large for huffman encoding
// We can safely shift input value for one bit -> log2(value << 1)
// Because of the 16 bit value constraint it won't overflow
// With that input value change we no longer need to add 1 before returning
// And this eliminates need to check if input value is zero - it is a standard convention which Log2SoftwareFallback adheres to
return Numerics.Log2(value << 1);
#endif
}
}
}

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

@ -49,7 +49,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
ref Block8x8F yBlock = ref this.Y;
ref L8 l8Start = ref l8Span[0];
for (int i = 0; i < 64; i++)
for (int i = 0; i < Block8x8F.Size; i++)
{
ref L8 c = ref Unsafe.Add(ref l8Start, i);
yBlock[i] = c.PackedValue;

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

@ -92,48 +92,144 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
return tables;
}
/// <summary>
/// Optimized method to allocates the correct y, cb, and cr values to the DCT blocks from the given r, g, b values.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ConvertPixelInto(
int r,
int g,
int b,
ref Block8x8F yResult,
ref Block8x8F cbResult,
ref Block8x8F crResult,
int i)
private float CalculateY(byte r, byte g, byte b)
{
// float y = (0.299F * r) + (0.587F * g) + (0.114F * b);
yResult[i] = (this.YRTable[r] + this.YGTable[g] + this.YBTable[b]) >> ScaleBits;
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));
cbResult[i] = (this.CbRTable[r] + this.CbGTable[g] + this.CbBTable[b]) >> ScaleBits;
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));
crResult[i] = (this.CbBTable[r] + this.CrGTable[g] + this.CrBTable[b]) >> ScaleBits;
return (this.CbBTable[r] + this.CrGTable[g] + this.CrBTable[b]) >> ScaleBits;
}
public void Convert(Span<Rgb24> rgbSpan, ref Block8x8F yBlock, ref Block8x8F cbBlock, ref Block8x8F crBlock)
/// <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 < 64; i++)
for (int i = 0; i < Block8x8F.Size; i++)
{
ref Rgb24 c = ref Unsafe.Add(ref rgbStart, i);
this.ConvertPixelInto(
c.R,
c.G,
c.B,
ref yBlock,
ref cbBlock,
ref crBlock,
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);

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

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
@ -27,19 +27,45 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
}
}
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
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
};
private static ReadOnlySpan<byte> MoveLast24BytesToSeparateLanes => new byte[]
{
2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0,
5, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, 1, 0, 0, 0
};
private static ReadOnlySpan<byte> ExtractRgb => new byte[]
{
0, 3, 6, 9, 1, 4, 7, 10, 2, 5, 8, 11, 0xFF, 0xFF, 0xFF, 0xFF,
@ -47,7 +73,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
};
#endif
public static void Convert(ReadOnlySpan<Rgb24> rgbSpan, ref Block8x8F yBlock, ref Block8x8F cbBlock, ref Block8x8F crBlock)
/// <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");
@ -63,18 +97,20 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
var f05 = Vector256.Create(0.5f);
var zero = Vector256.Create(0).AsByte();
ref Vector256<byte> inRef = ref Unsafe.As<Rgb24, Vector256<byte>>(ref MemoryMarshal.GetReference(rgbSpan));
ref Vector256<float> destYRef = ref Unsafe.As<Block8x8F, Vector256<float>>(ref yBlock);
ref Vector256<float> destCbRef = ref Unsafe.As<Block8x8F, Vector256<float>>(ref cbBlock);
ref Vector256<float> destCrRef = ref Unsafe.As<Block8x8F, Vector256<float>>(ref crBlock);
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;
for (int i = 0; i < 7; i++)
const int bytesPerRgbStride = 24;
for (int i = 0; i < 8; i++)
{
rgb = Avx2.PermuteVar8x32(Unsafe.AddByteOffset(ref inRef, (IntPtr)(24 * i)).AsUInt32(), extractToLanesMask).AsByte();
rgb = Avx2.PermuteVar8x32(Unsafe.AddByteOffset(ref rgbByteSpan, (IntPtr)(bytesPerRgbStride * i)).AsUInt32(), extractToLanesMask).AsByte();
rgb = Avx2.Shuffle(rgb, extractRgbMask);
@ -94,27 +130,130 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
// 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;
extractToLanesMask = Unsafe.As<byte, Vector256<uint>>(ref MemoryMarshal.GetReference(MoveLast24BytesToSeparateLanes));
rgb = Avx2.PermuteVar8x32(Unsafe.AddByteOffset(ref inRef, (IntPtr)160).AsUInt32(), extractToLanesMask).AsByte();
rgb = Avx2.Shuffle(rgb, extractRgbMask);
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);
rg = Avx2.UnpackLow(rgb, zero);
bx = Avx2.UnpackHigh(rgb, zero);
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());
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, 7) = SimdUtils.HwIntrinsics.MultiplyAdd(SimdUtils.HwIntrinsics.MultiplyAdd(Avx.Multiply(f0114, b), f0587, g), f0299, r);
int yBlockVerticalOffset = (i * 2) + ((j & 2) >> 1);
// 128F + ((-0.168736F * r) - (0.331264F * g) + (0.5F * b))
Unsafe.Add(ref destCbRef, 7) = Avx.Add(f128, SimdUtils.HwIntrinsics.MultiplyAdd(SimdUtils.HwIntrinsics.MultiplyAdd(Avx.Multiply(f05, b), fn0331264, g), fn0168736, r));
// (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);
// 128F + ((0.5F * r) - (0.418688F * g) - (0.081312F * b))
Unsafe.Add(ref destCrRef, 7) = Avx.Add(f128, SimdUtils.HwIntrinsics.MultiplyAdd(SimdUtils.HwIntrinsics.MultiplyAdd(Avx.Multiply(fn0081312F, b), fn0418688, g), f05, 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
}
}

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

@ -0,0 +1,121 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
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>
/// <see cref="Size"/> of sampling area from given frame pixel buffer
/// </summary>
private static readonly Size SampleSize = new Size(16, 8);
/// <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 Span<TPixel> pixelSpan;
/// <summary>
/// Temporal RGB block
/// </summary>
private Span<Rgb24> rgbSpan;
/// <summary>
/// Sampled pixel buffer size
/// </summary>
private Size samplingAreaSize;
/// <summary>
/// <see cref="Configuration"/> for internal operations
/// </summary>
private 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;
}
}
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

@ -0,0 +1,122 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
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>
/// <see cref="Size"/> of sampling area from given frame pixel buffer
/// </summary>
private static readonly Size SampleSize = new Size(8, 8);
/// <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 Span<TPixel> pixelSpan;
/// <summary>
/// Temporal 64-byte span to hold converted Rgb24 data
/// </summary>
private Span<Rgb24> rgbSpan;
/// <summary>
/// Sampled pixel buffer size
/// </summary>
private Size samplingAreaSize;
/// <summary>
/// <see cref="Configuration"/> for internal operations
/// </summary>
private 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>
/// 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);
}
}
}
}

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

@ -2,81 +2,59 @@
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Advanced;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
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 YCbCrForwardConverter<TPixel>
internal static class YCbCrForwardConverter<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// The Y component
/// </summary>
public Block8x8F Y;
/// <summary>
/// The Cb component
/// </summary>
public Block8x8F Cb;
/// <summary>
/// The Cr component
/// </summary>
public Block8x8F Cr;
public static void LoadAndStretchEdges(RowOctet<TPixel> source, Span<TPixel> dest, Point start, Size sampleSize, Size totalSize)
{
DebugGuard.MustBeBetweenOrEqualTo(start.X, 1, totalSize.Width - 1, nameof(start.X));
DebugGuard.MustBeBetweenOrEqualTo(start.Y, 1, totalSize.Height - 1, nameof(start.Y));
/// <summary>
/// The color conversion tables
/// </summary>
private RgbToYCbCrConverterLut colorTables;
int width = Math.Min(sampleSize.Width, totalSize.Width - start.X);
int height = Math.Min(sampleSize.Height, totalSize.Height - start.Y);
/// <summary>
/// Temporal 8x8 block to hold TPixel data
/// </summary>
private GenericBlock8x8<TPixel> pixelBlock;
uint byteWidth = (uint)(width * Unsafe.SizeOf<TPixel>());
int remainderXCount = sampleSize.Width - width;
/// <summary>
/// Temporal RGB block
/// </summary>
private GenericBlock8x8<Rgb24> rgbBlock;
ref byte blockStart = ref MemoryMarshal.GetReference(MemoryMarshal.Cast<TPixel, byte>(dest));
int rowSizeInBytes = sampleSize.Width * Unsafe.SizeOf<TPixel>();
public static YCbCrForwardConverter<TPixel> Create()
{
var result = default(YCbCrForwardConverter<TPixel>);
if (!RgbToYCbCrConverterVectorized.IsSupported)
for (int y = 0; y < height; y++)
{
// Avoid creating lookup tables, when vectorized converter is supported
result.colorTables = RgbToYCbCrConverterLut.Create();
}
Span<TPixel> row = source[y];
return result;
}
ref byte s = ref Unsafe.As<TPixel, byte>(ref row[start.X]);
ref byte d = ref Unsafe.Add(ref blockStart, y * rowSizeInBytes);
/// <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(ImageFrame<TPixel> frame, int x, int y, ref RowOctet<TPixel> currentRows)
{
this.pixelBlock.LoadAndStretchEdges(frame.PixelBuffer, x, y, ref currentRows);
Unsafe.CopyBlock(ref d, ref s, byteWidth);
ref TPixel last = ref Unsafe.Add(ref Unsafe.As<byte, TPixel>(ref d), width - 1);
Span<Rgb24> rgbSpan = this.rgbBlock.AsSpanUnsafe();
PixelOperations<TPixel>.Instance.ToRgb24(frame.GetConfiguration(), this.pixelBlock.AsSpanUnsafe(), rgbSpan);
for (int x = 1; x <= remainderXCount; x++)
{
Unsafe.Add(ref last, x) = last;
}
}
ref Block8x8F yBlock = ref this.Y;
ref Block8x8F cbBlock = ref this.Cb;
ref Block8x8F crBlock = ref this.Cr;
int remainderYCount = sampleSize.Height - height;
if (RgbToYCbCrConverterVectorized.IsSupported)
if (remainderYCount == 0)
{
RgbToYCbCrConverterVectorized.Convert(rgbSpan, ref yBlock, ref cbBlock, ref crBlock);
return;
}
else
ref byte lastRowStart = ref Unsafe.Add(ref blockStart, (height - 1) * rowSizeInBytes);
for (int y = 1; y <= remainderYCount; y++)
{
this.colorTables.Convert(rgbSpan, ref yBlock, ref cbBlock, ref crBlock);
ref byte remStart = ref Unsafe.Add(ref lastRowStart, rowSizeInBytes * y);
Unsafe.CopyBlock(ref remStart, ref lastRowStart, (uint)rowSizeInBytes);
}
}
}

463
src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.cs

@ -1,8 +1,13 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
#if SUPPORTS_RUNTIME_INTRINSICS
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
#endif
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Formats.Jpeg.Components
@ -10,7 +15,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
/// <summary>
/// Contains inaccurate, but fast forward and inverse DCT implementations.
/// </summary>
internal static class FastFloatingPointDCT
internal static partial class FastFloatingPointDCT
{
#pragma warning disable SA1310 // FieldNamesMustNotContainUnderscore
private const float C_1_175876 = 1.175875602f;
@ -38,147 +43,31 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
private const float C_0_765367 = 0.765366865f;
private const float C_0_125 = 0.1250f;
#if SUPPORTS_RUNTIME_INTRINSICS
private static readonly Vector256<float> C_V_0_5411 = Vector256.Create(0.541196f);
private static readonly Vector256<float> C_V_1_3065 = Vector256.Create(1.306563f);
private static readonly Vector256<float> C_V_1_1758 = Vector256.Create(1.175876f);
private static readonly Vector256<float> C_V_0_7856 = Vector256.Create(0.785695f);
private static readonly Vector256<float> C_V_1_3870 = Vector256.Create(1.387040f);
private static readonly Vector256<float> C_V_0_2758 = Vector256.Create(0.275899f);
private static readonly Vector256<float> C_V_n1_9615 = Vector256.Create(-1.961570560f);
private static readonly Vector256<float> C_V_n0_3901 = Vector256.Create(-0.390180644f);
private static readonly Vector256<float> C_V_n0_8999 = Vector256.Create(-0.899976223f);
private static readonly Vector256<float> C_V_n2_5629 = Vector256.Create(-2.562915447f);
private static readonly Vector256<float> C_V_0_2986 = Vector256.Create(0.298631336f);
private static readonly Vector256<float> C_V_2_0531 = Vector256.Create(2.053119869f);
private static readonly Vector256<float> C_V_3_0727 = Vector256.Create(3.072711026f);
private static readonly Vector256<float> C_V_1_5013 = Vector256.Create(1.501321110f);
private static readonly Vector256<float> C_V_n1_8477 = Vector256.Create(-1.847759065f);
private static readonly Vector256<float> C_V_0_7653 = Vector256.Create(0.765366865f);
private static readonly Vector256<float> C_V_InvSqrt2 = Vector256.Create(0.707107f);
#endif
#pragma warning restore SA1310 // FieldNamesMustNotContainUnderscore
private static readonly Vector4 InvSqrt2 = new Vector4(0.707107f);
/// <summary>
/// Apply floating point IDCT transformation into dest, using a temporary block 'temp' provided by the caller (optimization).
/// Ported from https://github.com/norishigefukushima/dct_simd/blob/master/dct/dct8x8_simd.cpp#L239
/// </summary>
/// <param name="src">Source</param>
/// <param name="dest">Destination</param>
/// <param name="temp">Temporary block provided by the caller</param>
public static void TransformIDCT(ref Block8x8F src, ref Block8x8F dest, ref Block8x8F temp)
{
src.TransposeInto(ref temp);
IDCT8x4_LeftPart(ref temp, ref dest);
IDCT8x4_RightPart(ref temp, ref dest);
dest.TransposeInto(ref temp);
IDCT8x4_LeftPart(ref temp, ref dest);
IDCT8x4_RightPart(ref temp, ref dest);
// TODO: What if we leave the blocks in a scaled-by-x8 state until final color packing?
dest.MultiplyInPlace(C_0_125);
}
/// <summary>
/// Do IDCT internal operations on the left part of the block. Original src:
/// https://github.com/norishigefukushima/dct_simd/blob/master/dct/dct8x8_simd.cpp#L261
/// </summary>
/// <param name="s">The source block</param>
/// <param name="d">Destination block</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void IDCT8x4_LeftPart(ref Block8x8F s, ref Block8x8F d)
{
Vector4 my1 = s.V1L;
Vector4 my7 = s.V7L;
Vector4 mz0 = my1 + my7;
Vector4 my3 = s.V3L;
Vector4 mz2 = my3 + my7;
Vector4 my5 = s.V5L;
Vector4 mz1 = my3 + my5;
Vector4 mz3 = my1 + my5;
Vector4 mz4 = (mz0 + mz1) * C_1_175876;
mz2 = (mz2 * C_1_961571) + mz4;
mz3 = (mz3 * C_0_390181) + mz4;
mz0 = mz0 * C_0_899976;
mz1 = mz1 * C_2_562915;
Vector4 mb3 = (my7 * C_0_298631) + mz0 + mz2;
Vector4 mb2 = (my5 * C_2_053120) + mz1 + mz3;
Vector4 mb1 = (my3 * C_3_072711) + mz1 + mz2;
Vector4 mb0 = (my1 * C_1_501321) + mz0 + mz3;
Vector4 my2 = s.V2L;
Vector4 my6 = s.V6L;
mz4 = (my2 + my6) * C_0_541196;
Vector4 my0 = s.V0L;
Vector4 my4 = s.V4L;
mz0 = my0 + my4;
mz1 = my0 - my4;
mz2 = mz4 + (my6 * C_1_847759);
mz3 = mz4 + (my2 * C_0_765367);
my0 = mz0 + mz3;
my3 = mz0 - mz3;
my1 = mz1 + mz2;
my2 = mz1 - mz2;
d.V0L = my0 + mb0;
d.V7L = my0 - mb0;
d.V1L = my1 + mb1;
d.V6L = my1 - mb1;
d.V2L = my2 + mb2;
d.V5L = my2 - mb2;
d.V3L = my3 + mb3;
d.V4L = my3 - mb3;
}
/// <summary>
/// Do IDCT internal operations on the right part of the block.
/// Original src:
/// https://github.com/norishigefukushima/dct_simd/blob/master/dct/dct8x8_simd.cpp#L261
/// </summary>
/// <param name="s">The source block</param>
/// <param name="d">The destination block</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void IDCT8x4_RightPart(ref Block8x8F s, ref Block8x8F d)
{
Vector4 my1 = s.V1R;
Vector4 my7 = s.V7R;
Vector4 mz0 = my1 + my7;
Vector4 my3 = s.V3R;
Vector4 mz2 = my3 + my7;
Vector4 my5 = s.V5R;
Vector4 mz1 = my3 + my5;
Vector4 mz3 = my1 + my5;
Vector4 mz4 = (mz0 + mz1) * C_1_175876;
mz2 = (mz2 * C_1_961571) + mz4;
mz3 = (mz3 * C_0_390181) + mz4;
mz0 = mz0 * C_0_899976;
mz1 = mz1 * C_2_562915;
Vector4 mb3 = (my7 * C_0_298631) + mz0 + mz2;
Vector4 mb2 = (my5 * C_2_053120) + mz1 + mz3;
Vector4 mb1 = (my3 * C_3_072711) + mz1 + mz2;
Vector4 mb0 = (my1 * C_1_501321) + mz0 + mz3;
Vector4 my2 = s.V2R;
Vector4 my6 = s.V6R;
mz4 = (my2 + my6) * C_0_541196;
Vector4 my0 = s.V0R;
Vector4 my4 = s.V4R;
mz0 = my0 + my4;
mz1 = my0 - my4;
mz2 = mz4 + (my6 * C_1_847759);
mz3 = mz4 + (my2 * C_0_765367);
my0 = mz0 + mz3;
my3 = mz0 - mz3;
my1 = mz1 + mz2;
my2 = mz1 - mz2;
d.V0R = my0 + mb0;
d.V7R = my0 - mb0;
d.V1R = my1 + mb1;
d.V6R = my1 - mb1;
d.V2R = my2 + mb2;
d.V5R = my2 - mb2;
d.V3R = my3 + mb3;
d.V4R = my3 - mb3;
}
/// <summary>
/// Original:
/// <see>
@ -309,11 +198,84 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
}
/// <summary>
/// Apply floating point IDCT transformation into dest, using a temporary block 'temp' provided by the caller (optimization)
/// Combined operation of <see cref="FDCT8x4_LeftPart(ref Block8x8F, ref Block8x8F)"/> and <see cref="FDCT8x4_RightPart(ref Block8x8F, ref Block8x8F)"/>
/// using AVX commands.
/// </summary>
/// <param name="s">Source</param>
/// <param name="d">Destination</param>
public static void FDCT8x8_Avx(ref Block8x8F s, ref Block8x8F d)
{
#if SUPPORTS_RUNTIME_INTRINSICS
Debug.Assert(Avx.IsSupported, "AVX is required to execute this method");
Vector256<float> t0 = Avx.Add(s.V0, s.V7);
Vector256<float> t7 = Avx.Subtract(s.V0, s.V7);
Vector256<float> t1 = Avx.Add(s.V1, s.V6);
Vector256<float> t6 = Avx.Subtract(s.V1, s.V6);
Vector256<float> t2 = Avx.Add(s.V2, s.V5);
Vector256<float> t5 = Avx.Subtract(s.V2, s.V5);
Vector256<float> t3 = Avx.Add(s.V3, s.V4);
Vector256<float> t4 = Avx.Subtract(s.V3, s.V4);
Vector256<float> c0 = Avx.Add(t0, t3);
Vector256<float> c1 = Avx.Add(t1, t2);
// 0 4
d.V0 = Avx.Add(c0, c1);
d.V4 = Avx.Subtract(c0, c1);
Vector256<float> c3 = Avx.Subtract(t0, t3);
Vector256<float> c2 = Avx.Subtract(t1, t2);
// 2 6
d.V2 = SimdUtils.HwIntrinsics.MultiplyAdd(Avx.Multiply(c2, C_V_0_5411), c3, C_V_1_3065);
d.V6 = SimdUtils.HwIntrinsics.MultiplySubstract(Avx.Multiply(c2, C_V_1_3065), c3, C_V_0_5411);
c3 = SimdUtils.HwIntrinsics.MultiplyAdd(Avx.Multiply(t4, C_V_1_1758), t7, C_V_0_7856);
c0 = SimdUtils.HwIntrinsics.MultiplySubstract(Avx.Multiply(t4, C_V_0_7856), t7, C_V_1_1758);
c2 = SimdUtils.HwIntrinsics.MultiplyAdd(Avx.Multiply(t5, C_V_1_3870), C_V_0_2758, t6);
c1 = SimdUtils.HwIntrinsics.MultiplySubstract(Avx.Multiply(C_V_0_2758, t5), t6, C_V_1_3870);
// 3 5
d.V3 = Avx.Subtract(c0, c2);
d.V5 = Avx.Subtract(c3, c1);
c0 = Avx.Multiply(Avx.Add(c0, c2), C_V_InvSqrt2);
c3 = Avx.Multiply(Avx.Add(c3, c1), C_V_InvSqrt2);
// 1 7
d.V1 = Avx.Add(c0, c3);
d.V7 = Avx.Subtract(c0, c3);
#endif
}
/// <summary>
/// Performs 8x8 matrix Forward Discrete Cosine Transform
/// </summary>
/// <param name="s">Source</param>
/// <param name="d">Destination</param>
public static void FDCT8x8(ref Block8x8F s, ref Block8x8F d)
{
#if SUPPORTS_RUNTIME_INTRINSICS
if (Avx.IsSupported)
{
FDCT8x8_Avx(ref s, ref d);
}
else
#endif
{
FDCT8x4_LeftPart(ref s, ref d);
FDCT8x4_RightPart(ref s, ref d);
}
}
/// <summary>
/// Apply floating point FDCT from src into dest
/// </summary>
/// <param name="src">Source</param>
/// <param name="dest">Destination</param>
/// <param name="temp">Temporary block provided by the caller</param>
/// <param name="temp">Temporary block provided by the caller for optimization</param>
/// <param name="offsetSourceByNeg128">If true, a constant -128.0 offset is applied for all values before FDCT </param>
public static void TransformFDCT(
ref Block8x8F src,
@ -327,14 +289,225 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
temp.AddInPlace(-128F);
}
FDCT8x4_LeftPart(ref temp, ref dest);
FDCT8x4_RightPart(ref temp, ref dest);
FDCT8x8(ref temp, ref dest);
dest.TransposeInto(ref temp);
FDCT8x4_LeftPart(ref temp, ref dest);
FDCT8x4_RightPart(ref temp, ref dest);
FDCT8x8(ref temp, ref dest);
dest.MultiplyInPlace(C_0_125);
}
/// <summary>
/// Performs 8x8 matrix Inverse Discrete Cosine Transform
/// </summary>
/// <param name="s">Source</param>
/// <param name="d">Destination</param>
public static void IDCT8x8(ref Block8x8F s, ref Block8x8F d)
{
#if SUPPORTS_RUNTIME_INTRINSICS
if (Avx.IsSupported)
{
IDCT8x8_Avx(ref s, ref d);
}
else
#endif
{
IDCT8x4_LeftPart(ref s, ref d);
IDCT8x4_RightPart(ref s, ref d);
}
}
/// <summary>
/// Do IDCT internal operations on the left part of the block. Original src:
/// https://github.com/norishigefukushima/dct_simd/blob/master/dct/dct8x8_simd.cpp#L261
/// </summary>
/// <param name="s">The source block</param>
/// <param name="d">Destination block</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void IDCT8x4_LeftPart(ref Block8x8F s, ref Block8x8F d)
{
Vector4 my1 = s.V1L;
Vector4 my7 = s.V7L;
Vector4 mz0 = my1 + my7;
Vector4 my3 = s.V3L;
Vector4 mz2 = my3 + my7;
Vector4 my5 = s.V5L;
Vector4 mz1 = my3 + my5;
Vector4 mz3 = my1 + my5;
Vector4 mz4 = (mz0 + mz1) * C_1_175876;
mz2 = (mz2 * C_1_961571) + mz4;
mz3 = (mz3 * C_0_390181) + mz4;
mz0 = mz0 * C_0_899976;
mz1 = mz1 * C_2_562915;
Vector4 mb3 = (my7 * C_0_298631) + mz0 + mz2;
Vector4 mb2 = (my5 * C_2_053120) + mz1 + mz3;
Vector4 mb1 = (my3 * C_3_072711) + mz1 + mz2;
Vector4 mb0 = (my1 * C_1_501321) + mz0 + mz3;
Vector4 my2 = s.V2L;
Vector4 my6 = s.V6L;
mz4 = (my2 + my6) * C_0_541196;
Vector4 my0 = s.V0L;
Vector4 my4 = s.V4L;
mz0 = my0 + my4;
mz1 = my0 - my4;
mz2 = mz4 + (my6 * C_1_847759);
mz3 = mz4 + (my2 * C_0_765367);
my0 = mz0 + mz3;
my3 = mz0 - mz3;
my1 = mz1 + mz2;
my2 = mz1 - mz2;
d.V0L = my0 + mb0;
d.V7L = my0 - mb0;
d.V1L = my1 + mb1;
d.V6L = my1 - mb1;
d.V2L = my2 + mb2;
d.V5L = my2 - mb2;
d.V3L = my3 + mb3;
d.V4L = my3 - mb3;
}
/// <summary>
/// Do IDCT internal operations on the right part of the block.
/// Original src:
/// https://github.com/norishigefukushima/dct_simd/blob/master/dct/dct8x8_simd.cpp#L261
/// </summary>
/// <param name="s">The source block</param>
/// <param name="d">The destination block</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void IDCT8x4_RightPart(ref Block8x8F s, ref Block8x8F d)
{
Vector4 my1 = s.V1R;
Vector4 my7 = s.V7R;
Vector4 mz0 = my1 + my7;
Vector4 my3 = s.V3R;
Vector4 mz2 = my3 + my7;
Vector4 my5 = s.V5R;
Vector4 mz1 = my3 + my5;
Vector4 mz3 = my1 + my5;
Vector4 mz4 = (mz0 + mz1) * C_1_175876;
mz2 = (mz2 * C_1_961571) + mz4;
mz3 = (mz3 * C_0_390181) + mz4;
mz0 = mz0 * C_0_899976;
mz1 = mz1 * C_2_562915;
Vector4 mb3 = (my7 * C_0_298631) + mz0 + mz2;
Vector4 mb2 = (my5 * C_2_053120) + mz1 + mz3;
Vector4 mb1 = (my3 * C_3_072711) + mz1 + mz2;
Vector4 mb0 = (my1 * C_1_501321) + mz0 + mz3;
Vector4 my2 = s.V2R;
Vector4 my6 = s.V6R;
mz4 = (my2 + my6) * C_0_541196;
Vector4 my0 = s.V0R;
Vector4 my4 = s.V4R;
mz0 = my0 + my4;
mz1 = my0 - my4;
mz2 = mz4 + (my6 * C_1_847759);
mz3 = mz4 + (my2 * C_0_765367);
my0 = mz0 + mz3;
my3 = mz0 - mz3;
my1 = mz1 + mz2;
my2 = mz1 - mz2;
d.V0R = my0 + mb0;
d.V7R = my0 - mb0;
d.V1R = my1 + mb1;
d.V6R = my1 - mb1;
d.V2R = my2 + mb2;
d.V5R = my2 - mb2;
d.V3R = my3 + mb3;
d.V4R = my3 - mb3;
}
/// <summary>
/// Combined operation of <see cref="IDCT8x4_LeftPart(ref Block8x8F, ref Block8x8F)"/> and <see cref="IDCT8x4_RightPart(ref Block8x8F, ref Block8x8F)"/>
/// using AVX commands.
/// </summary>
/// <param name="s">Source</param>
/// <param name="d">Destination</param>
public static void IDCT8x8_Avx(ref Block8x8F s, ref Block8x8F d)
{
#if SUPPORTS_RUNTIME_INTRINSICS
Debug.Assert(Avx.IsSupported, "AVX is required to execute this method");
Vector256<float> my1 = s.V1;
Vector256<float> my7 = s.V7;
Vector256<float> mz0 = Avx.Add(my1, my7);
Vector256<float> my3 = s.V3;
Vector256<float> mz2 = Avx.Add(my3, my7);
Vector256<float> my5 = s.V5;
Vector256<float> mz1 = Avx.Add(my3, my5);
Vector256<float> mz3 = Avx.Add(my1, my5);
Vector256<float> mz4 = Avx.Multiply(Avx.Add(mz0, mz1), C_V_1_1758);
mz2 = SimdUtils.HwIntrinsics.MultiplyAdd(mz4, mz2, C_V_n1_9615);
mz3 = SimdUtils.HwIntrinsics.MultiplyAdd(mz4, mz3, C_V_n0_3901);
mz0 = Avx.Multiply(mz0, C_V_n0_8999);
mz1 = Avx.Multiply(mz1, C_V_n2_5629);
Vector256<float> mb3 = Avx.Add(SimdUtils.HwIntrinsics.MultiplyAdd(mz0, my7, C_V_0_2986), mz2);
Vector256<float> mb2 = Avx.Add(SimdUtils.HwIntrinsics.MultiplyAdd(mz1, my5, C_V_2_0531), mz3);
Vector256<float> mb1 = Avx.Add(SimdUtils.HwIntrinsics.MultiplyAdd(mz1, my3, C_V_3_0727), mz2);
Vector256<float> mb0 = Avx.Add(SimdUtils.HwIntrinsics.MultiplyAdd(mz0, my1, C_V_1_5013), mz3);
Vector256<float> my2 = s.V2;
Vector256<float> my6 = s.V6;
mz4 = Avx.Multiply(Avx.Add(my2, my6), C_V_0_5411);
Vector256<float> my0 = s.V0;
Vector256<float> my4 = s.V4;
mz0 = Avx.Add(my0, my4);
mz1 = Avx.Subtract(my0, my4);
mz2 = SimdUtils.HwIntrinsics.MultiplyAdd(mz4, my6, C_V_n1_8477);
mz3 = SimdUtils.HwIntrinsics.MultiplyAdd(mz4, my2, C_V_0_7653);
my0 = Avx.Add(mz0, mz3);
my3 = Avx.Subtract(mz0, mz3);
my1 = Avx.Add(mz1, mz2);
my2 = Avx.Subtract(mz1, mz2);
d.V0 = Avx.Add(my0, mb0);
d.V7 = Avx.Subtract(my0, mb0);
d.V1 = Avx.Add(my1, mb1);
d.V6 = Avx.Subtract(my1, mb1);
d.V2 = Avx.Add(my2, mb2);
d.V5 = Avx.Subtract(my2, mb2);
d.V3 = Avx.Add(my3, mb3);
d.V4 = Avx.Subtract(my3, mb3);
#endif
}
/// <summary>
/// Apply floating point IDCT transformation into dest, using a temporary block 'temp' provided by the caller (optimization).
/// Ported from https://github.com/norishigefukushima/dct_simd/blob/master/dct/dct8x8_simd.cpp#L239
/// </summary>
/// <param name="src">Source</param>
/// <param name="dest">Destination</param>
/// <param name="temp">Temporary block provided by the caller</param>
public static void TransformIDCT(ref Block8x8F src, ref Block8x8F dest, ref Block8x8F temp)
{
src.TransposeInto(ref temp);
IDCT8x8(ref temp, ref dest);
dest.TransposeInto(ref temp);
IDCT8x8(ref temp, ref dest);
// TODO: What if we leave the blocks in a scaled-by-x8 state until final color packing?
dest.MultiplyInPlace(C_0_125);
}
}

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

@ -5,14 +5,11 @@ using System;
using System.Buffers.Binary;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
@ -36,17 +33,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
private readonly byte[] buffer = new byte[20];
/// <summary>
/// A buffer for reducing the number of stream writes when emitting Huffman tables. 64 seems to be enough.
/// </summary>
private readonly byte[] emitBuffer = new byte[64];
/// <summary>
/// A buffer for reducing the number of stream writes when emitting Huffman tables. Max combined table lengths +
/// identifier.
/// </summary>
private readonly byte[] huffmanBuffer = new byte[179];
/// <summary>
/// Gets or sets the subsampling method to use.
/// </summary>
@ -62,26 +48,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
private readonly JpegColorType? colorType;
/// <summary>
/// The accumulated bits to write to the stream.
/// </summary>
private uint accumulatedBits;
/// <summary>
/// The accumulated bit count.
/// </summary>
private uint bitCount;
/// <summary>
/// The scaled chrominance table, in zig-zag order.
/// </summary>
private Block8x8F chrominanceQuantTable;
/// <summary>
/// The scaled luminance table, in zig-zag order.
/// </summary>
private Block8x8F luminanceQuantTable;
/// <summary>
/// The output stream. All attempted writes after the first error become no-ops.
/// </summary>
@ -98,29 +64,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.colorType = options.ColorType;
}
/// <summary>
/// Gets the counts the number of bits needed to hold an integer.
/// </summary>
// The C# compiler emits this as a compile-time constant embedded in the PE file.
// This is effectively compiled down to: return new ReadOnlySpan<byte>(&data, length)
// More details can be found: https://github.com/dotnet/roslyn/pull/24621
private static ReadOnlySpan<byte> BitCountLut => new byte[]
{
0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8,
};
/// <summary>
/// Gets the unscaled quantization tables in zig-zag order. Each
/// encoder copies and scales the tables according to its quality parameter.
@ -131,14 +74,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// This is effectively compiled down to: return new ReadOnlySpan<byte>(&data, length)
// More details can be found: https://github.com/dotnet/roslyn/pull/24621
private static ReadOnlySpan<byte> UnscaledQuant_Luminance => new byte[]
{
// Luminance.
16, 11, 12, 14, 12, 10, 16, 14, 13, 14, 18, 17, 16, 19, 24,
40, 26, 24, 22, 22, 24, 49, 35, 37, 29, 40, 58, 51, 61, 60,
57, 51, 56, 55, 64, 72, 92, 78, 64, 68, 87, 69, 55, 56, 80,
109, 81, 87, 95, 98, 103, 104, 103, 62, 77, 113, 121, 112,
100, 120, 92, 101, 103, 99,
};
{
// Luminance.
16, 11, 12, 14, 12, 10, 16, 14, 13, 14, 18, 17, 16, 19, 24,
40, 26, 24, 22, 22, 24, 49, 35, 37, 29, 40, 58, 51, 61, 60,
57, 51, 56, 55, 64, 72, 92, 78, 64, 68, 87, 69, 55, 56, 80,
109, 81, 87, 95, 98, 103, 104, 103, 62, 77, 113, 121, 112,
100, 120, 92, 101, 103, 99,
};
/// <summary>
/// Gets the unscaled quantization tables in zig-zag order. Each
@ -150,14 +93,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// This is effectively compiled down to: return new ReadOnlySpan<byte>(&data, length)
// More details can be found: https://github.com/dotnet/roslyn/pull/24621
private static ReadOnlySpan<byte> UnscaledQuant_Chrominance => new byte[]
{
// Chrominance.
17, 18, 18, 24, 21, 24, 47, 26, 26, 47, 99, 66, 56, 66,
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
};
{
// Chrominance.
17, 18, 18, 24, 21, 24, 47, 26, 26, 47, 99, 66, 56, 66,
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
};
/// <summary>
/// Encode writes the image to the jpeg baseline format with the given options.
@ -171,14 +114,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
{
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
cancellationToken.ThrowIfCancellationRequested();
const ushort max = JpegConstants.MaxLength;
if (image.Width >= max || image.Height >= max)
if (image.Width >= JpegConstants.MaxLength || image.Height >= JpegConstants.MaxLength)
{
throw new ImageFormatException($"Image is too large to encode at {image.Width}x{image.Height}.");
JpegThrowHelper.ThrowDimensionsTooLarge(image.Width, image.Height);
}
cancellationToken.ThrowIfCancellationRequested();
this.outputStream = stream;
ImageMetadata metadata = image.Metadata;
@ -201,10 +144,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
// Initialize the quantization tables.
InitQuantizationTable(0, scale, ref this.luminanceQuantTable);
// TODO: This looks ugly, should we write chrominance table for luminance-only images?
// If not - this can code can be simplified
Block8x8F luminanceQuantTable = default;
Block8x8F chrominanceQuantTable = default;
InitQuantizationTable(0, scale, ref luminanceQuantTable);
if (componentCount > 1)
{
InitQuantizationTable(1, scale, ref this.chrominanceQuantTable);
InitQuantizationTable(1, scale, ref chrominanceQuantTable);
}
// Write the Start Of Image marker.
@ -214,7 +161,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.WriteProfiles(metadata);
// Write the quantization tables.
this.WriteDefineQuantizationTables();
this.WriteDefineQuantizationTables(ref luminanceQuantTable, ref chrominanceQuantTable);
// Write the image dimensions.
this.WriteStartOfFrame(image.Width, image.Height, componentCount);
@ -222,13 +169,31 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// Write the Huffman tables.
this.WriteDefineHuffmanTables(componentCount);
// Write the image data.
// Write the scan header.
this.WriteStartOfScan(image, componentCount, cancellationToken);
// Write the scan compressed data.
var scanEncoder = new HuffmanScanEncoder(stream);
if (this.colorType == JpegColorType.Luminance)
{
scanEncoder.EncodeGrayscale(image, ref luminanceQuantTable, cancellationToken);
}
else
{
switch (this.subsample)
{
case JpegSubsample.Ratio444:
scanEncoder.Encode444(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken);
break;
case JpegSubsample.Ratio420:
scanEncoder.Encode420(image, ref luminanceQuantTable, ref chrominanceQuantTable, cancellationToken);
break;
}
}
// Write the End Of Image marker.
this.buffer[0] = JpegConstants.Markers.XFF;
this.buffer[1] = JpegConstants.Markers.EOI;
stream.Write(this.buffer, 0, 2);
this.WriteEndOfImageMarker();
stream.Flush();
}
@ -248,248 +213,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
}
/// <summary>
/// Initializes quantization table.
/// </summary>
/// <param name="i">The quantization index.</param>
/// <param name="scale">The scaling factor.</param>
/// <param name="quant">The quantization table.</param>
private static void InitQuantizationTable(int i, int scale, ref Block8x8F quant)
{
DebugGuard.MustBeBetweenOrEqualTo(i, 0, 1, nameof(i));
ReadOnlySpan<byte> unscaledQuant = (i == 0) ? UnscaledQuant_Luminance : UnscaledQuant_Chrominance;
for (int j = 0; j < Block8x8F.Size; j++)
{
int x = unscaledQuant[j];
x = ((x * scale) + 50) / 100;
if (x < 1)
{
x = 1;
}
if (x > 255)
{
x = 255;
}
quant[j] = x;
}
}
/// <summary>
/// Emits the least significant count of bits of bits to the bit-stream.
/// The precondition is bits
/// <example>
/// &lt; 1&lt;&lt;nBits &amp;&amp; nBits &lt;= 16
/// </example>
/// .
/// </summary>
/// <param name="bits">The packed bits.</param>
/// <param name="count">The number of bits</param>
/// <param name="emitBufferBase">The reference to the emitBuffer.</param>
[MethodImpl(InliningOptions.ShortMethod)]
private void Emit(uint bits, uint count, ref byte emitBufferBase)
{
count += this.bitCount;
bits <<= (int)(32 - count);
bits |= this.accumulatedBits;
// Only write if more than 8 bits.
if (count >= 8)
{
// Track length
int len = 0;
while (count >= 8)
{
byte b = (byte)(bits >> 24);
Unsafe.Add(ref emitBufferBase, len++) = b;
if (b == byte.MaxValue)
{
Unsafe.Add(ref emitBufferBase, len++) = byte.MinValue;
}
bits <<= 8;
count -= 8;
}
if (len > 0)
{
this.outputStream.Write(this.emitBuffer, 0, len);
}
}
this.accumulatedBits = bits;
this.bitCount = count;
}
/// <summary>
/// Emits the given value with the given Huffman encoder.
/// </summary>
/// <param name="index">The index of the Huffman encoder</param>
/// <param name="value">The value to encode.</param>
/// <param name="emitBufferBase">The reference to the emit buffer.</param>
[MethodImpl(InliningOptions.ShortMethod)]
private void EmitHuff(HuffIndex index, int value, ref byte emitBufferBase)
{
uint x = HuffmanLut.TheHuffmanLut[(int)index].Values[value];
this.Emit(x & ((1 << 24) - 1), x >> 24, ref emitBufferBase);
}
/// <summary>
/// Emits a run of runLength copies of value encoded with the given Huffman encoder.
/// </summary>
/// <param name="index">The index of the Huffman encoder</param>
/// <param name="runLength">The number of copies to encode.</param>
/// <param name="value">The value to encode.</param>
/// <param name="emitBufferBase">The reference to the emit buffer.</param>
[MethodImpl(InliningOptions.ShortMethod)]
private void EmitHuffRLE(HuffIndex index, int runLength, int value, ref byte emitBufferBase)
{
int a = value;
int b = value;
if (a < 0)
{
a = -value;
b = value - 1;
}
uint bt;
if (a < 0x100)
{
bt = BitCountLut[a];
}
else
{
bt = 8 + (uint)BitCountLut[a >> 8];
}
this.EmitHuff(index, (int)((uint)(runLength << 4) | bt), ref emitBufferBase);
if (bt > 0)
{
this.Emit((uint)b & (uint)((1 << ((int)bt)) - 1), bt, ref emitBufferBase);
}
}
/// <summary>
/// Encodes the image with no subsampling.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The pixel accessor providing access to the image pixels.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param>
/// <param name="emitBufferBase">The reference to the emit buffer.</param>
private void Encode444<TPixel>(Image<TPixel> pixels, CancellationToken cancellationToken, ref byte emitBufferBase)
where TPixel : unmanaged, IPixel<TPixel>
{
// TODO: Need a JpegScanEncoder<TPixel> class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.)
// (Partially done with YCbCrForwardConverter<TPixel>)
Block8x8F temp1 = default;
Block8x8F temp2 = default;
Block8x8F onStackLuminanceQuantTable = this.luminanceQuantTable;
Block8x8F onStackChrominanceQuantTable = this.chrominanceQuantTable;
var unzig = ZigZag.CreateUnzigTable();
// ReSharper disable once InconsistentNaming
int prevDCY = 0, prevDCCb = 0, prevDCCr = 0;
var pixelConverter = YCbCrForwardConverter<TPixel>.Create();
ImageFrame<TPixel> frame = pixels.Frames.RootFrame;
Buffer2D<TPixel> pixelBuffer = frame.PixelBuffer;
RowOctet<TPixel> currentRows = default;
for (int y = 0; y < pixels.Height; y += 8)
{
cancellationToken.ThrowIfCancellationRequested();
currentRows.Update(pixelBuffer, y);
for (int x = 0; x < pixels.Width; x += 8)
{
pixelConverter.Convert(frame, x, y, ref currentRows);
prevDCY = this.WriteBlock(
QuantIndex.Luminance,
prevDCY,
ref pixelConverter.Y,
ref temp1,
ref temp2,
ref onStackLuminanceQuantTable,
ref unzig,
ref emitBufferBase);
prevDCCb = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCb,
ref pixelConverter.Cb,
ref temp1,
ref temp2,
ref onStackChrominanceQuantTable,
ref unzig,
ref emitBufferBase);
prevDCCr = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCr,
ref pixelConverter.Cr,
ref temp1,
ref temp2,
ref onStackChrominanceQuantTable,
ref unzig,
ref emitBufferBase);
}
}
}
/// <summary>
/// Encodes the image with no chroma, just luminance.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The pixel accessor providing access to the image pixels.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param>
/// <param name="emitBufferBase">The reference to the emit buffer.</param>
private void EncodeGrayscale<TPixel>(Image<TPixel> pixels, CancellationToken cancellationToken, ref byte emitBufferBase)
where TPixel : unmanaged, IPixel<TPixel>
{
// TODO: Need a JpegScanEncoder<TPixel> class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.)
// (Partially done with YCbCrForwardConverter<TPixel>)
Block8x8F temp1 = default;
Block8x8F temp2 = default;
Block8x8F onStackLuminanceQuantTable = this.luminanceQuantTable;
var unzig = ZigZag.CreateUnzigTable();
// ReSharper disable once InconsistentNaming
int prevDCY = 0;
var pixelConverter = LuminanceForwardConverter<TPixel>.Create();
ImageFrame<TPixel> frame = pixels.Frames.RootFrame;
Buffer2D<TPixel> pixelBuffer = frame.PixelBuffer;
RowOctet<TPixel> currentRows = default;
for (int y = 0; y < pixels.Height; y += 8)
{
cancellationToken.ThrowIfCancellationRequested();
currentRows.Update(pixelBuffer, y);
for (int x = 0; x < pixels.Width; x += 8)
{
pixelConverter.Convert(frame, x, y, ref currentRows);
prevDCY = this.WriteBlock(
QuantIndex.Luminance,
prevDCY,
ref pixelConverter.Y,
ref temp1,
ref temp2,
ref onStackLuminanceQuantTable,
ref unzig,
ref emitBufferBase);
}
}
}
/// <summary>
/// Writes the application header containing the JFIF identifier plus extra data.
/// </summary>
@ -539,72 +262,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.outputStream.Write(this.buffer, 0, 20);
}
/// <summary>
/// Writes a block of pixel data using the given quantization table,
/// returning the post-quantized DC value of the DCT-transformed block.
/// The block is in natural (not zig-zag) order.
/// </summary>
/// <param name="index">The quantization table index.</param>
/// <param name="prevDC">The previous DC value.</param>
/// <param name="src">Source block</param>
/// <param name="tempDest1">Temporal block to be used as FDCT Destination</param>
/// <param name="tempDest2">Temporal block 2</param>
/// <param name="quant">Quantization table</param>
/// <param name="unZig">The 8x8 Unzig block.</param>
/// <param name="emitBufferBase">The reference to the emit buffer.</param>
/// <returns>The <see cref="int"/>.</returns>
private int WriteBlock(
QuantIndex index,
int prevDC,
ref Block8x8F src,
ref Block8x8F tempDest1,
ref Block8x8F tempDest2,
ref Block8x8F quant,
ref ZigZag unZig,
ref byte emitBufferBase)
{
FastFloatingPointDCT.TransformFDCT(ref src, ref tempDest1, ref tempDest2);
Block8x8F.Quantize(ref tempDest1, ref tempDest2, ref quant, ref unZig);
int dc = (int)tempDest2[0];
// Emit the DC delta.
this.EmitHuffRLE((HuffIndex)((2 * (int)index) + 0), 0, dc - prevDC, ref emitBufferBase);
// Emit the AC components.
var h = (HuffIndex)((2 * (int)index) + 1);
int runLength = 0;
for (int zig = 1; zig < Block8x8F.Size; zig++)
{
int ac = (int)tempDest2[zig];
if (ac == 0)
{
runLength++;
}
else
{
while (runLength > 15)
{
this.EmitHuff(h, 0xf0, ref emitBufferBase);
runLength -= 16;
}
this.EmitHuffRLE(h, runLength, ac, ref emitBufferBase);
runLength = 0;
}
}
if (runLength > 0)
{
this.EmitHuff(h, 0x00, ref emitBufferBase);
}
return dc;
}
/// <summary>
/// Writes the Define Huffman Table marker and tables.
/// </summary>
@ -638,34 +295,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.WriteMarkerHeader(JpegConstants.Markers.DHT, markerlen);
for (int i = 0; i < specs.Length; i++)
{
ref HuffmanSpec spec = ref specs[i];
int len = 0;
fixed (byte* huffman = this.huffmanBuffer)
fixed (byte* count = spec.Count)
fixed (byte* values = spec.Values)
{
huffman[len++] = headers[i];
for (int c = 0; c < spec.Count.Length; c++)
{
huffman[len++] = count[c];
}
for (int v = 0; v < spec.Values.Length; v++)
{
huffman[len++] = values[v];
}
}
this.outputStream.Write(this.huffmanBuffer, 0, len);
this.outputStream.WriteByte(headers[i]);
this.outputStream.Write(specs[i].Count);
this.outputStream.Write(specs[i].Values);
}
}
/// <summary>
/// Writes the Define Quantization Marker and tables.
/// </summary>
private void WriteDefineQuantizationTables()
private void WriteDefineQuantizationTables(ref Block8x8F luminanceQuantTable, ref Block8x8F chrominanceQuantTable)
{
// Marker + quantization table lengths
int markerlen = 2 + (QuantizationTableCount * (1 + Block8x8F.Size));
@ -677,8 +316,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
byte[] dqt = new byte[dqtCount];
int offset = 0;
WriteDataToDqt(dqt, ref offset, QuantIndex.Luminance, ref this.luminanceQuantTable);
WriteDataToDqt(dqt, ref offset, QuantIndex.Chrominance, ref this.chrominanceQuantTable);
WriteDataToDqt(dqt, ref offset, QuantIndex.Luminance, ref luminanceQuantTable);
WriteDataToDqt(dqt, ref offset, QuantIndex.Chrominance, ref chrominanceQuantTable);
this.outputStream.Write(dqt, 0, dqtCount);
}
@ -982,7 +621,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
private void WriteStartOfScan<TPixel>(Image<TPixel> image, int componentCount, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
// TODO: Need a JpegScanEncoder<TPixel> class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.)
Span<byte> componentId = stackalloc byte[]
{
0x01,
@ -1024,111 +662,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.buffer[sosSize] = 0x3f; // Se - End of spectral selection.
this.buffer[sosSize + 1] = 0x00; // Ah + Ah (Successive approximation bit position high + low)
this.outputStream.Write(this.buffer, 0, sosSize + 2);
ref byte emitBufferBase = ref MemoryMarshal.GetReference<byte>(this.emitBuffer);
if (this.colorType == JpegColorType.Luminance)
{
this.EncodeGrayscale(image, cancellationToken, ref emitBufferBase);
}
else
{
switch (this.subsample)
{
case JpegSubsample.Ratio444:
this.Encode444(image, cancellationToken, ref emitBufferBase);
break;
case JpegSubsample.Ratio420:
this.Encode420(image, cancellationToken, ref emitBufferBase);
break;
}
}
// Pad the last byte with 1's.
this.Emit(0x7f, 7, ref emitBufferBase);
}
/// <summary>
/// Encodes the image with subsampling. The Cb and Cr components are each subsampled
/// at a factor of 2 both horizontally and vertically.
/// Writes the EndOfImage marker.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The pixel accessor providing access to the image pixels.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param>
/// <param name="emitBufferBase">The reference to the emit buffer.</param>
private void Encode420<TPixel>(Image<TPixel> pixels, CancellationToken cancellationToken, ref byte emitBufferBase)
where TPixel : unmanaged, IPixel<TPixel>
private void WriteEndOfImageMarker()
{
// TODO: Need a JpegScanEncoder<TPixel> class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.)
Block8x8F b = default;
Span<Block8x8F> cb = stackalloc Block8x8F[4];
Span<Block8x8F> cr = stackalloc Block8x8F[4];
Block8x8F temp1 = default;
Block8x8F temp2 = default;
Block8x8F onStackLuminanceQuantTable = this.luminanceQuantTable;
Block8x8F onStackChrominanceQuantTable = this.chrominanceQuantTable;
var unzig = ZigZag.CreateUnzigTable();
var pixelConverter = YCbCrForwardConverter<TPixel>.Create();
// 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;
for (int y = 0; y < pixels.Height; y += 16)
{
cancellationToken.ThrowIfCancellationRequested();
for (int x = 0; x < pixels.Width; x += 16)
{
for (int i = 0; i < 4; i++)
{
int xOff = (i & 1) * 8;
int yOff = (i & 2) * 4;
currentRows.Update(pixelBuffer, y + yOff);
pixelConverter.Convert(frame, x + xOff, y + yOff, ref currentRows);
cb[i] = pixelConverter.Cb;
cr[i] = pixelConverter.Cr;
prevDCY = this.WriteBlock(
QuantIndex.Luminance,
prevDCY,
ref pixelConverter.Y,
ref temp1,
ref temp2,
ref onStackLuminanceQuantTable,
ref unzig,
ref emitBufferBase);
}
Block8x8F.Scale16X16To8X8(ref b, cb);
prevDCCb = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCb,
ref b,
ref temp1,
ref temp2,
ref onStackChrominanceQuantTable,
ref unzig,
ref emitBufferBase);
Block8x8F.Scale16X16To8X8(ref b, cr);
prevDCCr = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCr,
ref b,
ref temp1,
ref temp2,
ref onStackChrominanceQuantTable,
ref unzig,
ref emitBufferBase);
}
}
this.buffer[0] = JpegConstants.Markers.XFF;
this.buffer[1] = JpegConstants.Markers.EOI;
this.outputStream.Write(this.buffer, 0, 2);
}
/// <summary>
@ -1145,5 +688,34 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.buffer[3] = (byte)(length & 0xff);
this.outputStream.Write(this.buffer, 0, 4);
}
/// <summary>
/// Initializes quantization table.
/// </summary>
/// <param name="i">The quantization index.</param>
/// <param name="scale">The scaling factor.</param>
/// <param name="quant">The quantization table.</param>
private static void InitQuantizationTable(int i, int scale, ref Block8x8F quant)
{
DebugGuard.MustBeBetweenOrEqualTo(i, 0, 1, nameof(i));
ReadOnlySpan<byte> unscaledQuant = (i == 0) ? UnscaledQuant_Luminance : UnscaledQuant_Chrominance;
for (int j = 0; j < Block8x8F.Size; j++)
{
int x = unscaledQuant[j];
x = ((x * scale) + 50) / 100;
if (x < 1)
{
x = 1;
}
if (x > 255)
{
x = 255;
}
quant[j] = x;
}
}
}
}

3
src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs

@ -46,5 +46,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
[MethodImpl(InliningOptions.ColdPath)]
public static void ThrowInvalidImageDimensions(int width, int height) => throw new InvalidImageContentException($"Invalid image dimensions: {width}x{height}.");
[MethodImpl(InliningOptions.ColdPath)]
public static void ThrowDimensionsTooLarge(int width, int height) => throw new ImageFormatException($"Image is too large to encode at {width}x{height} for JPEG format.");
}
}

4
src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs

@ -213,7 +213,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors
this.compressedDataBuffer = this.Allocator.Allocate<byte>(maxNeededBytes);
}
/// <summary>Writes a image compressed with CCITT T4 to the stream.</summary>
/// <summary>
/// Writes a image compressed with CCITT T4 to the stream.
/// </summary>
/// <param name="pixelsAsGray">The pixels as 8-bit gray array.</param>
/// <param name="height">The strip height.</param>
public override void CompressStrip(Span<byte> pixelsAsGray, int height)

58
src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs

@ -40,41 +40,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Constants
/// </summary>
public const int RowsPerStripInfinity = 2147483647;
/// <summary>
/// Size (in bytes) of the TIFF file header.
/// </summary>
public const int SizeOfTiffHeader = 8;
/// <summary>
/// Size (in bytes) of each individual TIFF IFD entry
/// </summary>
public const int SizeOfIfdEntry = 12;
/// <summary>
/// Size (in bytes) of the Short and SShort data types
/// </summary>
public const int SizeOfShort = 2;
/// <summary>
/// Size (in bytes) of the Long and SLong data types
/// </summary>
public const int SizeOfLong = 4;
/// <summary>
/// Size (in bytes) of the Rational and SRational data types
/// </summary>
public const int SizeOfRational = 8;
/// <summary>
/// Size (in bytes) of the Float data type
/// </summary>
public const int SizeOfFloat = 4;
/// <summary>
/// Size (in bytes) of the Double data type
/// </summary>
public const int SizeOfDouble = 8;
/// <summary>
/// The default strip size is 8k.
/// </summary>
@ -83,42 +53,22 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Constants
/// <summary>
/// The bits per sample for 1 bit bicolor images.
/// </summary>
public static readonly ushort[] BitsPerSample1Bit = { 1 };
public static readonly TiffBitsPerSample BitsPerSample1Bit = new TiffBitsPerSample(1, 0, 0);
/// <summary>
/// The bits per sample for images with a 4 color palette.
/// </summary>
public static readonly ushort[] BitsPerSample4Bit = { 4 };
public static readonly TiffBitsPerSample BitsPerSample4Bit = new TiffBitsPerSample(4, 0, 0);
/// <summary>
/// The bits per sample for 8 bit images.
/// </summary>
public static readonly ushort[] BitsPerSample8Bit = { 8 };
/// <summary>
/// The bits per sample for color images with 2 bits for each color channel.
/// </summary>
public static readonly ushort[] BitsPerSampleRgb2Bit = { 2, 2, 2 };
/// <summary>
/// The bits per sample for color images with 4 bits for each color channel.
/// </summary>
public static readonly ushort[] BitsPerSampleRgb4Bit = { 4, 4, 4 };
public static readonly TiffBitsPerSample BitsPerSample8Bit = new TiffBitsPerSample(8, 0, 0);
/// <summary>
/// The bits per sample for color images with 8 bits for each color channel.
/// </summary>
public static readonly ushort[] BitsPerSampleRgb8Bit = { 8, 8, 8 };
/// <summary>
/// The bits per sample for color images with 10 bits for each color channel.
/// </summary>
public static readonly ushort[] BitsPerSampleRgb10Bit = { 10, 10, 10 };
/// <summary>
/// The bits per sample for color images with 14 bits for each color channel.
/// </summary>
public static readonly ushort[] BitsPerSampleRgb14Bit = { 14, 14, 14 };
public static readonly TiffBitsPerSample BitsPerSampleRgb8Bit = new TiffBitsPerSample(8, 8, 8);
/// <summary>
/// The list of mimetypes that equate to a tiff.

4
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs

@ -19,9 +19,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
private readonly float factor;
public BlackIsZeroTiffColor(ushort[] bitsPerSample)
public BlackIsZeroTiffColor(TiffBitsPerSample bitsPerSample)
{
this.bitsPerSample0 = bitsPerSample[0];
this.bitsPerSample0 = bitsPerSample.Channel0;
this.factor = (1 << this.bitsPerSample0) - 1.0f;
}

4
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs

@ -21,9 +21,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
/// <param name="bitsPerSample">The number of bits per sample for each pixel.</param>
/// <param name="colorMap">The RGB color lookup table to use for decoding the image.</param>
public PaletteTiffColor(ushort[] bitsPerSample, ushort[] colorMap)
public PaletteTiffColor(TiffBitsPerSample bitsPerSample, ushort[] colorMap)
{
this.bitsPerSample0 = bitsPerSample[0];
this.bitsPerSample0 = bitsPerSample.Channel0;
int colorCount = 1 << this.bitsPerSample0;
this.palette = GeneratePalette(colorMap, colorCount);
}

8
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs

@ -26,11 +26,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
private readonly ushort bitsPerSampleB;
public RgbPlanarTiffColor(ushort[] bitsPerSample)
public RgbPlanarTiffColor(TiffBitsPerSample bitsPerSample)
{
this.bitsPerSampleR = bitsPerSample[0];
this.bitsPerSampleG = bitsPerSample[1];
this.bitsPerSampleB = bitsPerSample[2];
this.bitsPerSampleR = bitsPerSample.Channel0;
this.bitsPerSampleG = bitsPerSample.Channel1;
this.bitsPerSampleB = bitsPerSample.Channel2;
this.rFactor = (1 << this.bitsPerSampleR) - 1.0f;
this.gFactor = (1 << this.bitsPerSampleG) - 1.0f;

8
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs

@ -27,11 +27,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
private readonly ushort bitsPerSampleB;
public RgbTiffColor(ushort[] bitsPerSample)
public RgbTiffColor(TiffBitsPerSample bitsPerSample)
{
this.bitsPerSampleR = bitsPerSample[0];
this.bitsPerSampleG = bitsPerSample[1];
this.bitsPerSampleB = bitsPerSample[2];
this.bitsPerSampleR = bitsPerSample.Channel0;
this.bitsPerSampleG = bitsPerSample.Channel1;
this.bitsPerSampleB = bitsPerSample.Channel2;
this.rFactor = (1 << this.bitsPerSampleR) - 1.0f;
this.gFactor = (1 << this.bitsPerSampleG) - 1.0f;

83
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs

@ -8,107 +8,125 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
internal static class TiffColorDecoderFactory<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
public static TiffBaseColorDecoder<TPixel> Create(TiffColorType colorType, ushort[] bitsPerSample, ushort[] colorMap)
public static TiffBaseColorDecoder<TPixel> Create(TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap)
{
switch (colorType)
{
case TiffColorType.WhiteIsZero:
DebugGuard.IsTrue(bitsPerSample.Length == 1, "bitsPerSample");
DebugGuard.IsTrue(bitsPerSample.Channels == 1, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new WhiteIsZeroTiffColor<TPixel>(bitsPerSample);
case TiffColorType.WhiteIsZero1:
DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 1, "bitsPerSample");
DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 1, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new WhiteIsZero1TiffColor<TPixel>();
case TiffColorType.WhiteIsZero4:
DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 4, "bitsPerSample");
DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 4, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new WhiteIsZero4TiffColor<TPixel>();
case TiffColorType.WhiteIsZero8:
DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 8, "bitsPerSample");
DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 8, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new WhiteIsZero8TiffColor<TPixel>();
case TiffColorType.BlackIsZero:
DebugGuard.IsTrue(bitsPerSample.Length == 1, "bitsPerSample");
DebugGuard.IsTrue(bitsPerSample.Channels == 1, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new BlackIsZeroTiffColor<TPixel>(bitsPerSample);
case TiffColorType.BlackIsZero1:
DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 1, "bitsPerSample");
DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 1, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new BlackIsZero1TiffColor<TPixel>();
case TiffColorType.BlackIsZero4:
DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 4, "bitsPerSample");
DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 4, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new BlackIsZero4TiffColor<TPixel>();
case TiffColorType.BlackIsZero8:
DebugGuard.IsTrue(bitsPerSample.Length == 1 && bitsPerSample[0] == 8, "bitsPerSample");
DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 8, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new BlackIsZero8TiffColor<TPixel>();
case TiffColorType.Rgb:
DebugGuard.NotNull(bitsPerSample, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new RgbTiffColor<TPixel>(bitsPerSample);
case TiffColorType.Rgb222:
DebugGuard.IsTrue(
bitsPerSample.Length == 3
&& bitsPerSample[2] == 2
&& bitsPerSample[1] == 2
&& bitsPerSample[0] == 2,
bitsPerSample.Channels == 3
&& bitsPerSample.Channel2 == 2
&& bitsPerSample.Channel1 == 2
&& bitsPerSample.Channel0 == 2,
"bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new RgbTiffColor<TPixel>(bitsPerSample);
case TiffColorType.Rgb444:
DebugGuard.IsTrue(
bitsPerSample.Length == 3
&& bitsPerSample[2] == 4
&& bitsPerSample[1] == 4
&& bitsPerSample[0] == 4,
bitsPerSample.Channels == 3
&& bitsPerSample.Channel2 == 4
&& bitsPerSample.Channel1 == 4
&& bitsPerSample.Channel0 == 4,
"bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new Rgb444TiffColor<TPixel>();
case TiffColorType.Rgb888:
DebugGuard.IsTrue(
bitsPerSample.Length == 3
&& bitsPerSample[2] == 8
&& bitsPerSample[1] == 8
&& bitsPerSample[0] == 8,
bitsPerSample.Channels == 3
&& bitsPerSample.Channel2 == 8
&& bitsPerSample.Channel1 == 8
&& bitsPerSample.Channel0 == 8,
"bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new Rgb888TiffColor<TPixel>();
case TiffColorType.Rgb101010:
DebugGuard.IsTrue(
bitsPerSample.Length == 3
&& bitsPerSample[2] == 10
&& bitsPerSample[1] == 10
&& bitsPerSample[0] == 10,
bitsPerSample.Channels == 3
&& bitsPerSample.Channel2 == 10
&& bitsPerSample.Channel1 == 10
&& bitsPerSample.Channel0 == 10,
"bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new RgbTiffColor<TPixel>(bitsPerSample);
case TiffColorType.Rgb121212:
DebugGuard.IsTrue(
bitsPerSample.Channels == 3
&& bitsPerSample.Channel2 == 12
&& bitsPerSample.Channel1 == 12
&& bitsPerSample.Channel0 == 12,
"bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new RgbTiffColor<TPixel>(bitsPerSample);
case TiffColorType.Rgb141414:
DebugGuard.IsTrue(
bitsPerSample.Length == 3
&& bitsPerSample[2] == 14
&& bitsPerSample[1] == 14
&& bitsPerSample[0] == 14,
bitsPerSample.Channels == 3
&& bitsPerSample.Channel2 == 14
&& bitsPerSample.Channel1 == 14
&& bitsPerSample.Channel0 == 14,
"bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new RgbTiffColor<TPixel>(bitsPerSample);
case TiffColorType.Rgb161616:
DebugGuard.IsTrue(
bitsPerSample.Channels == 3
&& bitsPerSample.Channel2 == 16
&& bitsPerSample.Channel1 == 16
&& bitsPerSample.Channel0 == 16,
"bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new RgbTiffColor<TPixel>(bitsPerSample);
case TiffColorType.PaletteColor:
DebugGuard.NotNull(bitsPerSample, "bitsPerSample");
DebugGuard.NotNull(colorMap, "colorMap");
return new PaletteTiffColor<TPixel>(bitsPerSample, colorMap);
@ -117,12 +135,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
}
}
public static RgbPlanarTiffColor<TPixel> CreatePlanar(TiffColorType colorType, ushort[] bitsPerSample, ushort[] colorMap)
public static RgbPlanarTiffColor<TPixel> CreatePlanar(TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap)
{
switch (colorType)
{
case TiffColorType.RgbPlanar:
DebugGuard.NotNull(bitsPerSample, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new RgbPlanarTiffColor<TPixel>(bitsPerSample);

10
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs

@ -78,11 +78,21 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
/// </summary>
Rgb101010,
/// <summary>
/// RGB color image with 12 bits for each channel.
/// </summary>
Rgb121212,
/// <summary>
/// RGB color image with 14 bits for each channel.
/// </summary>
Rgb141414,
/// <summary>
/// RGB color image with 16 bits for each channel.
/// </summary>
Rgb161616,
/// <summary>
/// RGB Full Color. Planar configuration of data.
/// </summary>

4
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs

@ -19,9 +19,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
private readonly float factor;
public WhiteIsZeroTiffColor(ushort[] bitsPerSample)
public WhiteIsZeroTiffColor(TiffBitsPerSample bitsPerSample)
{
this.bitsPerSample0 = bitsPerSample[0];
this.bitsPerSample0 = bitsPerSample.Channel0;
this.factor = (float)Math.Pow(2, this.bitsPerSample0) - 1.0f;
}

35
src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs

@ -30,6 +30,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary>
Bit8 = 8,
/// <summary>
/// 10 bits per pixel, for gray images.
///
/// Note: The TiffEncoder does not yet support 10 bits per pixel and will default to 24 bits per pixel instead.
/// </summary>
Bit10 = 10,
/// <summary>
/// 12 bits per pixel. 4 bit for each color channel.
///
@ -37,6 +44,20 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary>
Bit12 = 12,
/// <summary>
/// 14 bits per pixel, for gray images.
///
/// Note: The TiffEncoder does not yet support 14 bits per pixel images and will default to 24 bits per pixel instead.
/// </summary>
Bit14 = 14,
/// <summary>
/// 16 bits per pixel, for gray images.
///
/// Note: The TiffEncoder does not yet support 16 bits per color channel and will default to 24 bits per pixel instead.
/// </summary>
Bit16 = 16,
/// <summary>
/// 24 bits per pixel. One byte for each color channel.
/// </summary>
@ -49,11 +70,25 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary>
Bit30 = 30,
/// <summary>
/// 36 bits per pixel. 12 bit for each color channel.
///
/// Note: The TiffEncoder does not yet support 12 bits per color channel and will default to 24 bits per pixel instead.
/// </summary>
Bit36 = 36,
/// <summary>
/// 42 bits per pixel. 14 bit for each color channel.
///
/// Note: The TiffEncoder does not yet support 14 bits per color channel and will default to 24 bits per pixel instead.
/// </summary>
Bit42 = 42,
/// <summary>
/// 48 bits per pixel. 16 bit for each color channel.
///
/// Note: The TiffEncoder does not yet support 16 bits per color channel and will default to 24 bits per pixel instead.
/// </summary>
Bit48 = 48,
}
}

124
src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs

@ -1,56 +1,138 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
namespace SixLabors.ImageSharp.Formats.Tiff
{
/// <summary>
/// The number of bits per component.
/// </summary>
public enum TiffBitsPerSample
public readonly struct TiffBitsPerSample : IEquatable<TiffBitsPerSample>
{
/// <summary>
/// The Bits per samples is not known.
/// The bits for the channel 0.
/// </summary>
Unknown = 0,
public readonly ushort Channel0;
/// <summary>
/// One bit per sample for bicolor images.
/// The bits for the channel 1.
/// </summary>
Bit1 = 1,
public readonly ushort Channel1;
/// <summary>
/// Four bits per sample for grayscale images with 16 different levels of gray or paletted images with a palette of 16 colors.
/// The bits for the channel 2.
/// </summary>
Bit4 = 4,
public readonly ushort Channel2;
/// <summary>
/// Eight bits per sample for grayscale images with 256 different levels of gray or paletted images with a palette of 256 colors.
/// The number of channels.
/// </summary>
Bit8 = 8,
public readonly byte Channels;
/// <summary>
/// Six bits per sample, each channel has 2 bits.
/// Initializes a new instance of the <see cref="TiffBitsPerSample"/> struct.
/// </summary>
Bit6 = 6,
/// <param name="channel0">The bits for the channel 0.</param>
/// <param name="channel1">The bits for the channel 1.</param>
/// <param name="channel2">The bits for the channel 2.</param>
public TiffBitsPerSample(ushort channel0, ushort channel1, ushort channel2)
{
this.Channel0 = (ushort)Numerics.Clamp(channel0, 0, 32);
this.Channel1 = (ushort)Numerics.Clamp(channel1, 0, 32);
this.Channel2 = (ushort)Numerics.Clamp(channel2, 0, 32);
/// <summary>
/// Twelve bits per sample, each channel has 4 bits.
/// </summary>
Bit12 = 12,
this.Channels = 0;
this.Channels += (byte)(this.Channel0 != 0 ? 1 : 0);
this.Channels += (byte)(this.Channel1 != 0 ? 1 : 0);
this.Channels += (byte)(this.Channel2 != 0 ? 1 : 0);
}
/// <summary>
/// 24 bits per sample, each color channel has 8 Bits.
/// Tries to parse a ushort array and convert it into a TiffBitsPerSample struct.
/// </summary>
Bit24 = 24,
/// <param name="value">The value to parse.</param>
/// <param name="sample">The tiff bits per sample.</param>
/// <returns>True, if the value could be parsed.</returns>
public static bool TryParse(ushort[] value, out TiffBitsPerSample sample)
{
if (value is null || value.Length == 0)
{
sample = default;
return false;
}
ushort c2;
ushort c1;
ushort c0;
switch (value.Length)
{
case 3:
c2 = value[2];
c1 = value[1];
c0 = value[0];
break;
case 2:
c2 = 0;
c1 = value[1];
c0 = value[0];
break;
default:
c2 = 0;
c1 = 0;
c0 = value[0];
break;
}
sample = new TiffBitsPerSample(c0, c1, c2);
return true;
}
/// <inheritdoc/>
public override bool Equals(object obj)
=> obj is TiffBitsPerSample sample && this.Equals(sample);
/// <inheritdoc/>
public bool Equals(TiffBitsPerSample other)
=> this.Channel0 == other.Channel0
&& this.Channel1 == other.Channel1
&& this.Channel2 == other.Channel2;
/// <inheritdoc/>
public override int GetHashCode()
=> HashCode.Combine(this.Channel0, this.Channel1, this.Channel2);
/// <summary>
/// Thirty bits per sample, each channel has 10 bits.
/// Converts the bits per sample struct to an ushort array.
/// </summary>
Bit30 = 30,
/// <returns>Bits per sample as ushort array.</returns>
public ushort[] ToArray()
{
if (this.Channel1 == 0)
{
return new[] { this.Channel0 };
}
if (this.Channel2 == 0)
{
return new[] { this.Channel0, this.Channel1 };
}
return new[] { this.Channel0, this.Channel1, this.Channel2 };
}
/// <summary>
/// Forty two bits per sample, each channel has 14 bits.
/// Gets the bits per pixel for the given bits per sample.
/// </summary>
Bit42 = 42,
/// <returns>Bits per pixel.</returns>
public TiffBitsPerPixel BitsPerPixel()
{
int bitsPerPixel = this.Channel0 + this.Channel1 + this.Channel2;
return (TiffBitsPerPixel)bitsPerPixel;
}
/// <inheritdoc/>
public override string ToString()
=> $"TiffBitsPerSample({this.Channel0}, {this.Channel1}, {this.Channel2})";
}
}

111
src/ImageSharp/Formats/Tiff/TiffBitsPerSampleExtensions.cs

@ -1,111 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
namespace SixLabors.ImageSharp.Formats.Tiff
{
internal static class TiffBitsPerSampleExtensions
{
/// <summary>
/// Gets the bits per channel array for a given BitsPerSample value, e,g, for RGB888: [8, 8, 8]
/// </summary>
/// <param name="tiffBitsPerSample">The tiff bits per sample.</param>
/// <returns>Bits per sample array.</returns>
public static ushort[] Bits(this TiffBitsPerSample tiffBitsPerSample)
{
switch (tiffBitsPerSample)
{
case TiffBitsPerSample.Bit1:
return TiffConstants.BitsPerSample1Bit;
case TiffBitsPerSample.Bit4:
return TiffConstants.BitsPerSample4Bit;
case TiffBitsPerSample.Bit6:
return TiffConstants.BitsPerSampleRgb2Bit;
case TiffBitsPerSample.Bit8:
return TiffConstants.BitsPerSample8Bit;
case TiffBitsPerSample.Bit12:
return TiffConstants.BitsPerSampleRgb4Bit;
case TiffBitsPerSample.Bit24:
return TiffConstants.BitsPerSampleRgb8Bit;
case TiffBitsPerSample.Bit30:
return TiffConstants.BitsPerSampleRgb10Bit;
case TiffBitsPerSample.Bit42:
return TiffConstants.BitsPerSampleRgb14Bit;
default:
return Array.Empty<ushort>();
}
}
/// <summary>
/// Maps an array of bits per sample to a concrete enum value.
/// </summary>
/// <param name="bitsPerSample">The bits per sample array.</param>
/// <returns>TiffBitsPerSample enum value.</returns>
public static TiffBitsPerSample GetBitsPerSample(this ushort[] bitsPerSample)
{
switch (bitsPerSample.Length)
{
case 3:
if (bitsPerSample[2] == TiffConstants.BitsPerSampleRgb14Bit[2] &&
bitsPerSample[1] == TiffConstants.BitsPerSampleRgb14Bit[1] &&
bitsPerSample[0] == TiffConstants.BitsPerSampleRgb14Bit[0])
{
return TiffBitsPerSample.Bit42;
}
if (bitsPerSample[2] == TiffConstants.BitsPerSampleRgb10Bit[2] &&
bitsPerSample[1] == TiffConstants.BitsPerSampleRgb10Bit[1] &&
bitsPerSample[0] == TiffConstants.BitsPerSampleRgb10Bit[0])
{
return TiffBitsPerSample.Bit30;
}
if (bitsPerSample[2] == TiffConstants.BitsPerSampleRgb8Bit[2] &&
bitsPerSample[1] == TiffConstants.BitsPerSampleRgb8Bit[1] &&
bitsPerSample[0] == TiffConstants.BitsPerSampleRgb8Bit[0])
{
return TiffBitsPerSample.Bit24;
}
if (bitsPerSample[2] == TiffConstants.BitsPerSampleRgb4Bit[2] &&
bitsPerSample[1] == TiffConstants.BitsPerSampleRgb4Bit[1] &&
bitsPerSample[0] == TiffConstants.BitsPerSampleRgb4Bit[0])
{
return TiffBitsPerSample.Bit12;
}
if (bitsPerSample[2] == TiffConstants.BitsPerSampleRgb2Bit[2] &&
bitsPerSample[1] == TiffConstants.BitsPerSampleRgb2Bit[1] &&
bitsPerSample[0] == TiffConstants.BitsPerSampleRgb2Bit[0])
{
return TiffBitsPerSample.Bit6;
}
break;
case 1:
if (bitsPerSample[0] == TiffConstants.BitsPerSample1Bit[0])
{
return TiffBitsPerSample.Bit1;
}
if (bitsPerSample[0] == TiffConstants.BitsPerSample4Bit[0])
{
return TiffBitsPerSample.Bit4;
}
if (bitsPerSample[0] == TiffConstants.BitsPerSample8Bit[0])
{
return TiffBitsPerSample.Bit8;
}
break;
}
return TiffBitsPerSample.Unknown;
}
}
}

28
src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

@ -50,7 +50,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
}
/// <summary>
/// Gets or sets the number of bits per component of the pixel format used to decode the image.
/// Gets or sets the bits per sample.
/// </summary>
public TiffBitsPerSample BitsPerSample { get; set; }
@ -198,7 +198,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// <returns>The size (in bytes) of the required pixel buffer.</returns>
private int CalculateStripBufferSize(int width, int height, int plane = -1)
{
int bitsPerPixel;
DebugGuard.MustBeLessThanOrEqualTo(plane, 3, nameof(plane));
int bitsPerPixel = 0;
if (this.PlanarConfiguration == TiffPlanarConfiguration.Chunky)
{
@ -207,7 +209,21 @@ namespace SixLabors.ImageSharp.Formats.Tiff
}
else
{
bitsPerPixel = this.BitsPerSample.Bits()[plane];
switch (plane)
{
case 0:
bitsPerPixel = this.BitsPerSample.Channel0;
break;
case 1:
bitsPerPixel = this.BitsPerSample.Channel1;
break;
case 2:
bitsPerPixel = this.BitsPerSample.Channel2;
break;
default:
TiffThrowHelper.ThrowNotSupported("More then 3 color channels are not supported");
break;
}
}
int bytesPerRow = ((width * bitsPerPixel) + 7) / 8;
@ -225,7 +241,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
private void DecodeStripsPlanar<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts)
where TPixel : unmanaged, IPixel<TPixel>
{
int stripsPerPixel = this.BitsPerSample.Bits().Length;
int stripsPerPixel = this.BitsPerSample.Channels;
int stripsPerPlane = stripOffsets.Length / stripsPerPixel;
int bitsPerPixel = this.BitsPerPixel;
@ -243,7 +259,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, frame.Width, bitsPerPixel, this.Predictor, this.FaxCompressionOptions);
RgbPlanarTiffColor<TPixel> colorDecoder = TiffColorDecoderFactory<TPixel>.CreatePlanar(this.ColorType, this.BitsPerSample.Bits(), this.ColorMap);
RgbPlanarTiffColor<TPixel> colorDecoder = TiffColorDecoderFactory<TPixel>.CreatePlanar(this.ColorType, this.BitsPerSample, this.ColorMap);
for (int i = 0; i < stripsPerPlane; i++)
{
@ -286,7 +302,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, frame.Width, bitsPerPixel, this.Predictor, this.FaxCompressionOptions);
TiffBaseColorDecoder<TPixel> colorDecoder = TiffColorDecoderFactory<TPixel>.Create(this.ColorType, this.BitsPerSample.Bits(), this.ColorMap);
TiffBaseColorDecoder<TPixel> colorDecoder = TiffColorDecoderFactory<TPixel>.Create(this.ColorType, this.BitsPerSample, this.ColorMap);
for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++)
{

73
src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Linq;
using SixLabors.ImageSharp.Formats.Tiff.Compression;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
@ -69,7 +68,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
options.Predictor = frameMetadata.Predictor ?? TiffPredictor.None;
options.PhotometricInterpretation = frameMetadata.PhotometricInterpretation ?? TiffPhotometricInterpretation.Rgb;
options.BitsPerPixel = frameMetadata.BitsPerPixel != null ? (int)frameMetadata.BitsPerPixel.Value : (int)TiffBitsPerPixel.Bit24;
options.BitsPerSample = GetBitsPerSample(frameMetadata.BitsPerPixel);
options.BitsPerSample = frameMetadata.BitsPerSample ?? new TiffBitsPerSample(0, 0, 0);
options.ParseColorType(exifProfile);
options.ParseCompression(frameMetadata.Compression, exifProfile);
@ -99,26 +98,32 @@ namespace SixLabors.ImageSharp.Formats.Tiff
{
case TiffPhotometricInterpretation.WhiteIsZero:
{
if (options.BitsPerSample.Bits().Length != 1)
if (options.BitsPerSample.Channels != 1)
{
TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported.");
}
switch (options.BitsPerSample)
ushort bitsPerChannel = options.BitsPerSample.Channel0;
if (bitsPerChannel > 16)
{
case TiffBitsPerSample.Bit8:
TiffThrowHelper.ThrowNotSupported("Bits per sample is not supported.");
}
switch (bitsPerChannel)
{
case 8:
{
options.ColorType = TiffColorType.WhiteIsZero8;
break;
}
case TiffBitsPerSample.Bit4:
case 4:
{
options.ColorType = TiffColorType.WhiteIsZero4;
break;
}
case TiffBitsPerSample.Bit1:
case 1:
{
options.ColorType = TiffColorType.WhiteIsZero1;
break;
@ -136,26 +141,32 @@ namespace SixLabors.ImageSharp.Formats.Tiff
case TiffPhotometricInterpretation.BlackIsZero:
{
if (options.BitsPerSample.Bits().Length != 1)
if (options.BitsPerSample.Channels != 1)
{
TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported.");
}
switch (options.BitsPerSample)
ushort bitsPerChannel = options.BitsPerSample.Channel0;
if (bitsPerChannel > 16)
{
TiffThrowHelper.ThrowNotSupported("Bits per sample is not supported.");
}
switch (bitsPerChannel)
{
case TiffBitsPerSample.Bit8:
case 8:
{
options.ColorType = TiffColorType.BlackIsZero8;
break;
}
case TiffBitsPerSample.Bit4:
case 4:
{
options.ColorType = TiffColorType.BlackIsZero4;
break;
}
case TiffBitsPerSample.Bit1:
case 1:
{
options.ColorType = TiffColorType.BlackIsZero1;
break;
@ -173,30 +184,39 @@ namespace SixLabors.ImageSharp.Formats.Tiff
case TiffPhotometricInterpretation.Rgb:
{
if (options.BitsPerSample.Bits().Length != 3)
if (options.BitsPerSample.Channels != 3)
{
TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported.");
}
if (options.PlanarConfiguration == TiffPlanarConfiguration.Chunky)
{
switch (options.BitsPerSample)
ushort bitsPerChannel = options.BitsPerSample.Channel0;
switch (bitsPerChannel)
{
case TiffBitsPerSample.Bit42:
case 16:
options.ColorType = TiffColorType.Rgb161616;
break;
case 14:
options.ColorType = TiffColorType.Rgb141414;
break;
case TiffBitsPerSample.Bit30:
case 12:
options.ColorType = TiffColorType.Rgb121212;
break;
case 10:
options.ColorType = TiffColorType.Rgb101010;
break;
case TiffBitsPerSample.Bit24:
case 8:
options.ColorType = TiffColorType.Rgb888;
break;
case TiffBitsPerSample.Bit12:
case 4:
options.ColorType = TiffColorType.Rgb444;
break;
case TiffBitsPerSample.Bit6:
case 2:
options.ColorType = TiffColorType.Rgb222;
break;
default:
@ -217,7 +237,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
options.ColorMap = exifProfile.GetValue(ExifTag.ColorMap)?.Value;
if (options.ColorMap != null)
{
if (options.BitsPerSample.Bits().Length != 1)
if (options.BitsPerSample.Channels != 1)
{
TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported.");
}
@ -291,18 +311,5 @@ namespace SixLabors.ImageSharp.Formats.Tiff
}
}
}
private static TiffBitsPerSample GetBitsPerSample(TiffBitsPerPixel? bitsPerPixel) => bitsPerPixel switch
{
TiffBitsPerPixel.Bit1 => TiffBitsPerSample.Bit1,
TiffBitsPerPixel.Bit4 => TiffBitsPerSample.Bit4,
TiffBitsPerPixel.Bit6 => TiffBitsPerSample.Bit6,
TiffBitsPerPixel.Bit8 => TiffBitsPerSample.Bit8,
TiffBitsPerPixel.Bit12 => TiffBitsPerSample.Bit12,
TiffBitsPerPixel.Bit24 => TiffBitsPerSample.Bit24,
TiffBitsPerPixel.Bit30 => TiffBitsPerSample.Bit30,
TiffBitsPerPixel.Bit42 => TiffBitsPerSample.Bit42,
_ => throw new NotSupportedException("The bits per pixel are not supported"),
};
}
}

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

@ -320,10 +320,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff
this.SetEncoderOptions(bitsPerPixel, photometricInterpretation ?? TiffPhotometricInterpretation.BlackIsZero, compression, predictor);
break;
case TiffBitsPerPixel.Bit6:
case TiffBitsPerPixel.Bit10:
case TiffBitsPerPixel.Bit12:
case TiffBitsPerPixel.Bit14:
case TiffBitsPerPixel.Bit16:
case TiffBitsPerPixel.Bit30:
case TiffBitsPerPixel.Bit36:
case TiffBitsPerPixel.Bit42:
// Encoding 42, 30, 12 and 6 bits per pixel is not yet supported. Default to 24 bits.
case TiffBitsPerPixel.Bit48:
// Encoding not yet supported bits per pixel will default to 24 bits.
this.SetEncoderOptions(TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, compression, TiffPredictor.None);
break;
default:

16
src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs

@ -318,34 +318,34 @@ namespace SixLabors.ImageSharp.Formats.Tiff
case TiffPhotometricInterpretation.PaletteColor:
if (encoder.BitsPerPixel == TiffBitsPerPixel.Bit4)
{
return TiffConstants.BitsPerSample4Bit;
return TiffConstants.BitsPerSample4Bit.ToArray();
}
else
{
return TiffConstants.BitsPerSample8Bit;
return TiffConstants.BitsPerSample8Bit.ToArray();
}
case TiffPhotometricInterpretation.Rgb:
return TiffConstants.BitsPerSampleRgb8Bit;
return TiffConstants.BitsPerSampleRgb8Bit.ToArray();
case TiffPhotometricInterpretation.WhiteIsZero:
if (encoder.BitsPerPixel == TiffBitsPerPixel.Bit1)
{
return TiffConstants.BitsPerSample1Bit;
return TiffConstants.BitsPerSample1Bit.ToArray();
}
return TiffConstants.BitsPerSample8Bit;
return TiffConstants.BitsPerSample8Bit.ToArray();
case TiffPhotometricInterpretation.BlackIsZero:
if (encoder.BitsPerPixel == TiffBitsPerPixel.Bit1)
{
return TiffConstants.BitsPerSample1Bit;
return TiffConstants.BitsPerSample1Bit.ToArray();
}
return TiffConstants.BitsPerSample8Bit;
return TiffConstants.BitsPerSample8Bit.ToArray();
default:
return TiffConstants.BitsPerSampleRgb8Bit;
return TiffConstants.BitsPerSampleRgb8Bit.ToArray();
}
}

39
src/ImageSharp/Formats/Tiff/TiffFrameMetadata.cs

@ -35,6 +35,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary>
public TiffBitsPerPixel? BitsPerPixel { get; set; }
/// <summary>
/// Gets or sets number of bits per component.
/// </summary>
public TiffBitsPerSample? BitsPerSample { get; set; }
/// <summary>
/// Gets or sets the compression scheme used on the image data.
/// </summary>
@ -64,7 +69,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
}
/// <summary>
/// Parses the given Exif profile to populate the properties of the tiff frame meta data..
/// Parses the given Exif profile to populate the properties of the tiff frame meta data.
/// </summary>
/// <param name="meta">The tiff frame meta data.</param>
/// <param name="profile">The Exif profile containing tiff frame directory tags.</param>
@ -72,11 +77,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff
{
if (profile != null)
{
ushort[] bitsPerSample = profile.GetValue(ExifTag.BitsPerSample)?.Value;
meta.BitsPerPixel = BitsPerPixelFromBitsPerSample(bitsPerSample);
if (TiffBitsPerSample.TryParse(profile.GetValue(ExifTag.BitsPerSample)?.Value, out TiffBitsPerSample bitsPerSample))
{
meta.BitsPerSample = bitsPerSample;
}
meta.BitsPerPixel = meta.BitsPerSample?.BitsPerPixel();
meta.Compression = (TiffCompression?)profile.GetValue(ExifTag.Compression)?.Value;
meta.PhotometricInterpretation =
(TiffPhotometricInterpretation?)profile.GetValue(ExifTag.PhotometricInterpretation)?.Value;
meta.PhotometricInterpretation = (TiffPhotometricInterpretation?)profile.GetValue(ExifTag.PhotometricInterpretation)?.Value;
meta.Predictor = (TiffPredictor?)profile.GetValue(ExifTag.Predictor)?.Value;
profile.RemoveValue(ExifTag.BitsPerSample);
@ -86,27 +94,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff
}
}
/// <summary>
/// Gets the bits per pixel for the given bits per sample.
/// </summary>
/// <param name="bitsPerSample">The tiff bits per sample.</param>
/// <returns>Bits per pixel.</returns>
private static TiffBitsPerPixel? BitsPerPixelFromBitsPerSample(ushort[] bitsPerSample)
{
if (bitsPerSample == null)
{
return null;
}
int bitsPerPixel = 0;
foreach (ushort bits in bitsPerSample)
{
bitsPerPixel += bits;
}
return (TiffBitsPerPixel)bitsPerPixel;
}
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new TiffFrameMetadata(this);
}

4
src/ImageSharp/Formats/Tiff/Writers/TiffBaseColorWriter{TPixel}.cs

@ -79,10 +79,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers
this.Dispose(true);
}
protected static Span<T> GetStripPixels<T>(Buffer2D<T> buffer2D, int y, int height)
where T : struct
=> buffer2D.GetSingleSpan().Slice(y * buffer2D.Width, height * buffer2D.Width);
protected abstract void EncodeStrip(int y, int height, TiffBaseCompressor compressor);
/// <summary>

38
src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs

@ -36,38 +36,50 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers
/// <inheritdoc/>
protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor)
{
this.pixelsAsGray ??= this.MemoryAllocator.Allocate<byte>(height * this.Image.Width);
Span<byte> pixelAsGraySpan = this.pixelsAsGray.Slice(0, height * this.Image.Width);
Span<TPixel> pixelsBlackWhite = GetStripPixels(this.imageBlackWhite.GetRootFramePixelBuffer(), y, height);
PixelOperations<TPixel>.Instance.ToL8Bytes(this.Configuration, pixelsBlackWhite, pixelAsGraySpan, pixelsBlackWhite.Length);
int width = this.Image.Width;
if (compressor.Method == TiffCompression.CcittGroup3Fax || compressor.Method == TiffCompression.Ccitt1D)
{
// Special case for T4BitCompressor.
compressor.CompressStrip(pixelAsGraySpan, height);
int stripPixels = width * height;
this.pixelsAsGray ??= this.MemoryAllocator.Allocate<byte>(stripPixels);
Span<byte> pixelAsGraySpan = this.pixelsAsGray.GetSpan();
int lastRow = y + height;
int grayRowIdx = 0;
for (int row = y; row < lastRow; row++)
{
Span<TPixel> pixelsBlackWhiteRow = this.imageBlackWhite.GetPixelRowSpan(row);
Span<byte> pixelAsGrayRow = pixelAsGraySpan.Slice(grayRowIdx * width, width);
PixelOperations<TPixel>.Instance.ToL8Bytes(this.Configuration, pixelsBlackWhiteRow, pixelAsGrayRow, width);
grayRowIdx++;
}
compressor.CompressStrip(pixelAsGraySpan.Slice(0, stripPixels), height);
}
else
{
// Write uncompressed image.
int bytesPerStrip = this.BytesPerRow * height;
this.bitStrip ??= this.MemoryAllocator.AllocateManagedByteBuffer(bytesPerStrip);
this.pixelsAsGray ??= this.MemoryAllocator.Allocate<byte>(width);
Span<byte> pixelAsGraySpan = this.pixelsAsGray.GetSpan();
Span<byte> rows = this.bitStrip.Slice(0, bytesPerStrip);
rows.Clear();
int grayPixelIndex = 0;
for (int s = 0; s < height; s++)
int outputRowIdx = 0;
int lastRow = y + height;
for (int row = y; row < lastRow; row++)
{
int bitIndex = 0;
int byteIndex = 0;
Span<byte> outputRow = rows.Slice(s * this.BytesPerRow);
Span<byte> outputRow = rows.Slice(outputRowIdx * this.BytesPerRow);
Span<TPixel> pixelsBlackWhiteRow = this.imageBlackWhite.GetPixelRowSpan(row);
PixelOperations<TPixel>.Instance.ToL8Bytes(this.Configuration, pixelsBlackWhiteRow, pixelAsGraySpan, width);
for (int x = 0; x < this.Image.Width; x++)
{
int shift = 7 - bitIndex;
if (pixelAsGraySpan[grayPixelIndex++] == 255)
if (pixelAsGraySpan[x] == 255)
{
outputRow[byteIndex] |= (byte)(1 << shift);
}
@ -79,6 +91,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers
bitIndex = 0;
}
}
outputRowIdx++;
}
compressor.CompressStrip(rows, height);

19
src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using SixLabors.ImageSharp.Formats.Tiff.Compression;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -30,12 +31,22 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers
this.rowBuffer.Clear();
Span<byte> rowSpan = this.rowBuffer.GetSpan().Slice(0, this.BytesPerRow * height);
Span<byte> outputRowSpan = this.rowBuffer.GetSpan().Slice(0, this.BytesPerRow * height);
Span<TPixel> pixels = GetStripPixels(this.Image.PixelBuffer, y, height);
int width = this.Image.Width;
using IMemoryOwner<TPixel> stripPixelBuffer = this.MemoryAllocator.Allocate<TPixel>(height * width);
Span<TPixel> stripPixels = stripPixelBuffer.GetSpan();
int lastRow = y + height;
int stripPixelsRowIdx = 0;
for (int row = y; row < lastRow; row++)
{
Span<TPixel> stripPixelsRow = this.Image.PixelBuffer.GetRowSpan(row);
stripPixelsRow.CopyTo(stripPixels.Slice(stripPixelsRowIdx * width, width));
stripPixelsRowIdx++;
}
this.EncodePixels(pixels, rowSpan);
compressor.CompressStrip(rowSpan, height);
this.EncodePixels(stripPixels, outputRowSpan);
compressor.CompressStrip(outputRowSpan, height);
}
protected abstract void EncodePixels(Span<TPixel> pixels, Span<byte> buffer);

40
src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs

@ -5,7 +5,6 @@ using System;
using System.Buffers;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Formats.Tiff.Compression;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
@ -21,6 +20,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers
private readonly int colorPaletteSize;
private readonly int colorPaletteBytes;
private readonly IndexedImageFrame<TPixel> quantizedImage;
private IMemoryOwner<byte> indexedPixelsBuffer;
public TiffPaletteWriter(
ImageFrame<TPixel> image,
@ -55,22 +55,24 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers
/// <inheritdoc />
protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor)
{
Span<byte> indexedPixels = GetStripPixels(((IPixelSource)this.quantizedImage).PixelBuffer, y, height);
int width = this.Image.Width;
if (this.BitsPerPixel == 4)
{
int width = this.Image.Width;
int halfWidth = width >> 1;
int excess = (width & 1) * height; // (width % 2) * height
int rows4BitBufferLength = (halfWidth * height) + excess;
using IMemoryOwner<byte> rows4bitBuffer = this.MemoryAllocator.Allocate<byte>(rows4BitBufferLength);
Span<byte> rows4bit = rows4bitBuffer.GetSpan();
int idxPixels = 0;
this.indexedPixelsBuffer ??= this.MemoryAllocator.Allocate<byte>(rows4BitBufferLength);
Span<byte> rows4bit = this.indexedPixelsBuffer.GetSpan();
int idx4bitRows = 0;
for (int row = 0; row < height; row++)
int lastRow = y + height;
for (int row = y; row < lastRow; row++)
{
ReadOnlySpan<byte> indexedPixelRow = this.quantizedImage.GetPixelRowSpan(row);
int idxPixels = 0;
for (int x = 0; x < halfWidth; x++)
{
rows4bit[idx4bitRows] = (byte)((indexedPixels[idxPixels] << 4) | (indexedPixels[idxPixels + 1] & 0xF));
rows4bit[idx4bitRows] = (byte)((indexedPixelRow[idxPixels] << 4) | (indexedPixelRow[idxPixels + 1] & 0xF));
idxPixels += 2;
idx4bitRows++;
}
@ -78,7 +80,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers
// Make sure rows are byte-aligned.
if (width % 2 != 0)
{
rows4bit[idx4bitRows++] = (byte)(indexedPixels[idxPixels++] << 4);
rows4bit[idx4bitRows++] = (byte)(indexedPixelRow[idxPixels] << 4);
}
}
@ -86,12 +88,28 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers
}
else
{
compressor.CompressStrip(indexedPixels, height);
int stripPixels = width * height;
this.indexedPixelsBuffer ??= this.MemoryAllocator.AllocateManagedByteBuffer(stripPixels);
Span<byte> indexedPixels = this.indexedPixelsBuffer.GetSpan();
int lastRow = y + height;
int indexedPixelsRowIdx = 0;
for (int row = y; row < lastRow; row++)
{
ReadOnlySpan<byte> indexedPixelRow = this.quantizedImage.GetPixelRowSpan(row);
indexedPixelRow.CopyTo(indexedPixels.Slice(indexedPixelsRowIdx * width, width));
indexedPixelsRowIdx++;
}
compressor.CompressStrip(indexedPixels.Slice(0, stripPixels), height);
}
}
/// <inheritdoc />
protected override void Dispose(bool disposing) => this.quantizedImage?.Dispose();
protected override void Dispose(bool disposing)
{
this.quantizedImage?.Dispose();
this.indexedPixelsBuffer?.Dispose();
}
private void AddColorMapTag()
{

2
src/ImageSharp/ImageSharp.csproj

@ -12,7 +12,7 @@
<PackageProjectUrl>$(RepositoryUrl)</PackageProjectUrl>
<PackageTags>Image Resize Crop Gif Jpg Jpeg Bitmap Png Tga NetCore</PackageTags>
<Description>A new, fully featured, fully managed, cross-platform, 2D graphics API for .NET</Description>
<Configurations>Debug;Release;Release-InnerLoop;Debug-InnerLoop</Configurations>
<Configurations>Debug;Release;Debug-InnerLoop;Release-InnerLoop</Configurations>
</PropertyGroup>
<Choose>

10
src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
@ -131,10 +131,6 @@ namespace SixLabors.ImageSharp.Memory
Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length));
int itemSizeBytes = Unsafe.SizeOf<T>();
int bufferSizeInBytes = length * itemSizeBytes;
if (bufferSizeInBytes < 0 || bufferSizeInBytes > this.BufferCapacityInBytes)
{
ThrowInvalidAllocationException<T>(length);
}
ArrayPool<byte> pool = this.GetArrayPool(bufferSizeInBytes);
byte[] byteArray = pool.Rent(bufferSizeInBytes);
@ -171,9 +167,9 @@ namespace SixLabors.ImageSharp.Memory
}
[MethodImpl(InliningOptions.ColdPath)]
private static void ThrowInvalidAllocationException<T>(int length) =>
private static void ThrowInvalidAllocationException<T>(int length, int max) =>
throw new InvalidMemoryOperationException(
$"Requested allocation: {length} elements of {typeof(T).Name} is over the capacity of the MemoryAllocator.");
$"Requested allocation: '{length}' elements of '{typeof(T).Name}' is over the capacity in bytes '{max}' of the MemoryAllocator.");
private ArrayPool<byte> GetArrayPool(int bufferSizeInBytes)
{

16
src/ImageSharp/Processing/Extensions/Dithering/DitherExtensions.cs

@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Processing
Dither(source, KnownDitherings.Bayer8x8);
/// <summary>
/// Dithers the image reducing it to a web-safe palette using ordered dithering.
/// Dithers the image reducing it to a web-safe palette.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="dither">The ordered ditherer.</param>
@ -32,7 +32,7 @@ namespace SixLabors.ImageSharp.Processing
source.ApplyProcessor(new PaletteDitherProcessor(dither));
/// <summary>
/// Dithers the image reducing it to a web-safe palette using ordered dithering.
/// Dithers the image reducing it to a web-safe palette.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="dither">The ordered ditherer.</param>
@ -45,7 +45,7 @@ namespace SixLabors.ImageSharp.Processing
source.ApplyProcessor(new PaletteDitherProcessor(dither, ditherScale));
/// <summary>
/// Dithers the image reducing it to the given palette using ordered dithering.
/// Dithers the image reducing it to the given palette.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="dither">The ordered ditherer.</param>
@ -58,7 +58,7 @@ namespace SixLabors.ImageSharp.Processing
source.ApplyProcessor(new PaletteDitherProcessor(dither, palette));
/// <summary>
/// Dithers the image reducing it to the given palette using ordered dithering.
/// Dithers the image reducing it to the given palette.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="dither">The ordered ditherer.</param>
@ -84,7 +84,7 @@ namespace SixLabors.ImageSharp.Processing
Dither(source, KnownDitherings.Bayer8x8, rectangle);
/// <summary>
/// Dithers the image reducing it to a web-safe palette using ordered dithering.
/// Dithers the image reducing it to a web-safe palette.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="dither">The ordered ditherer.</param>
@ -99,7 +99,7 @@ namespace SixLabors.ImageSharp.Processing
source.ApplyProcessor(new PaletteDitherProcessor(dither), rectangle);
/// <summary>
/// Dithers the image reducing it to a web-safe palette using ordered dithering.
/// Dithers the image reducing it to a web-safe palette.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="dither">The ordered ditherer.</param>
@ -116,7 +116,7 @@ namespace SixLabors.ImageSharp.Processing
source.ApplyProcessor(new PaletteDitherProcessor(dither, ditherScale), rectangle);
/// <summary>
/// Dithers the image reducing it to the given palette using ordered dithering.
/// Dithers the image reducing it to the given palette.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="dither">The ordered ditherer.</param>
@ -133,7 +133,7 @@ namespace SixLabors.ImageSharp.Processing
source.ApplyProcessor(new PaletteDitherProcessor(dither, palette), rectangle);
/// <summary>
/// Dithers the image reducing it to the given palette using ordered dithering.
/// Dithers the image reducing it to the given palette.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="dither">The ordered ditherer.</param>

136
src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs

@ -3,7 +3,6 @@
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -110,17 +109,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
where TFrameQuantizer : struct, IQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
var ditherOperation = new QuantizeDitherRowOperation<TFrameQuantizer, TPixel>(
ref quantizer,
in Unsafe.AsRef(this),
source,
destination,
bounds);
int spread = CalculatePaletteSpread(destination.Palette.Length);
float scale = quantizer.Options.DitherScale;
ParallelRowIterator.IterateRows(
quantizer.Configuration,
bounds,
in ditherOperation);
for (int y = bounds.Top; y < bounds.Bottom; y++)
{
ReadOnlySpan<TPixel> sourceRow = source.GetPixelRowSpan(y).Slice(bounds.X, bounds.Width);
Span<byte> destRow = destination.GetWritablePixelRowSpanUnsafe(y - bounds.Y).Slice(0, sourceRow.Length);
for (int x = 0; x < sourceRow.Length; x++)
{
TPixel dithered = this.Dither(sourceRow[x], x, y, spread, scale);
destRow[x] = quantizer.GetQuantizedColor(dithered, out TPixel _);
}
}
}
/// <inheritdoc/>
@ -132,16 +134,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
var ditherOperation = new PaletteDitherRowOperation<TPaletteDitherImageProcessor, TPixel>(
in processor,
in Unsafe.AsRef(this),
source,
bounds);
int spread = CalculatePaletteSpread(processor.Palette.Length);
float scale = processor.DitherScale;
ParallelRowIterator.IterateRows(
processor.Configuration,
bounds,
in ditherOperation);
for (int y = bounds.Top; y < bounds.Bottom; y++)
{
Span<TPixel> row = source.GetPixelRowSpan(y).Slice(bounds.X, bounds.Width);
for (int x = 0; x < row.Length; x++)
{
ref TPixel sourcePixel = ref row[x];
TPixel dithered = this.Dither(sourcePixel, x, y, spread, scale);
sourcePixel = processor.GetPaletteColor(dithered);
}
}
}
// Spread assumes an even colorspace distribution and precision.
@ -195,95 +201,5 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
[MethodImpl(InliningOptions.ShortMethod)]
public override int GetHashCode()
=> HashCode.Combine(this.thresholdMatrix, this.modulusX, this.modulusY);
private readonly struct QuantizeDitherRowOperation<TFrameQuantizer, TPixel> : IRowOperation
where TFrameQuantizer : struct, IQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly TFrameQuantizer quantizer;
private readonly OrderedDither dither;
private readonly ImageFrame<TPixel> source;
private readonly IndexedImageFrame<TPixel> destination;
private readonly Rectangle bounds;
private readonly int spread;
[MethodImpl(InliningOptions.ShortMethod)]
public QuantizeDitherRowOperation(
ref TFrameQuantizer quantizer,
in OrderedDither dither,
ImageFrame<TPixel> source,
IndexedImageFrame<TPixel> destination,
Rectangle bounds)
{
this.quantizer = quantizer;
this.dither = dither;
this.source = source;
this.destination = destination;
this.bounds = bounds;
this.spread = CalculatePaletteSpread(destination.Palette.Length);
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y)
{
ref TFrameQuantizer quantizer = ref Unsafe.AsRef(this.quantizer);
int spread = this.spread;
float scale = this.quantizer.Options.DitherScale;
ReadOnlySpan<TPixel> sourceRow = this.source.GetPixelRowSpan(y).Slice(this.bounds.X, this.bounds.Width);
Span<byte> destRow =
this.destination.GetWritablePixelRowSpanUnsafe(y - this.bounds.Y).Slice(0, sourceRow.Length);
for (int x = 0; x < sourceRow.Length; x++)
{
TPixel dithered = this.dither.Dither(sourceRow[x], x, y, spread, scale);
destRow[x] = quantizer.GetQuantizedColor(dithered, out TPixel _);
}
}
}
private readonly struct PaletteDitherRowOperation<TPaletteDitherImageProcessor, TPixel> : IRowOperation
where TPaletteDitherImageProcessor : struct, IPaletteDitherImageProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly TPaletteDitherImageProcessor processor;
private readonly OrderedDither dither;
private readonly ImageFrame<TPixel> source;
private readonly Rectangle bounds;
private readonly float scale;
private readonly int spread;
[MethodImpl(InliningOptions.ShortMethod)]
public PaletteDitherRowOperation(
in TPaletteDitherImageProcessor processor,
in OrderedDither dither,
ImageFrame<TPixel> source,
Rectangle bounds)
{
this.processor = processor;
this.dither = dither;
this.source = source;
this.bounds = bounds;
this.scale = processor.DitherScale;
this.spread = CalculatePaletteSpread(processor.Palette.Length);
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y)
{
ref TPaletteDitherImageProcessor processor = ref Unsafe.AsRef(this.processor);
int spread = this.spread;
float scale = this.scale;
Span<TPixel> row = this.source.GetPixelRowSpan(y).Slice(this.bounds.X, this.bounds.Width);
for (int x = 0; x < row.Length; x++)
{
ref TPixel sourcePixel = ref row[x];
TPixel dithered = this.dither.Dither(sourcePixel, x, y, spread, scale);
sourcePixel = processor.GetPaletteColor(dithered);
}
}
}
}
}

5
src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs

@ -62,6 +62,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
if (disposing)
{
this.paletteOwner.Dispose();
this.ditherProcessor.Dispose();
}
this.paletteOwner = null;
@ -73,7 +74,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
/// <see cref="IPaletteDitherImageProcessor{TPixel}.GetPaletteColor(TPixel)"/>.
/// </summary>
/// <remarks>Internal for AOT</remarks>
internal readonly struct DitherProcessor : IPaletteDitherImageProcessor<TPixel>
internal readonly struct DitherProcessor : IPaletteDitherImageProcessor<TPixel>, IDisposable
{
private readonly EuclideanPixelMap<TPixel> pixelMap;
@ -101,6 +102,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
this.pixelMap.GetClosestColor(color, out TPixel match);
return match;
}
public void Dispose() => this.pixelMap.Dispose();
}
}
}

134
src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs

@ -2,10 +2,10 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Concurrent;
using System.Numerics;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Quantization
@ -14,11 +14,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// Gets the closest color to the supplied color based upon the Euclidean distance.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal readonly struct EuclideanPixelMap<TPixel>
/// <para>
/// This class is not threadsafe and should not be accessed in parallel.
/// Doing so will result in non-idempotent results.
/// </para>
internal readonly struct EuclideanPixelMap<TPixel> : IDisposable
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly Vector4[] vectorCache;
private readonly ConcurrentDictionary<TPixel, int> distanceCache;
private readonly Rgba32[] rgbaPalette;
private readonly ColorDistanceCache cache;
/// <summary>
/// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel}"/> struct.
@ -29,11 +33,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory<TPixel> palette)
{
this.Palette = palette;
this.vectorCache = new Vector4[palette.Length];
// Use the same rules across all target frameworks.
this.distanceCache = new ConcurrentDictionary<TPixel, int>(Environment.ProcessorCount, 31);
PixelOperations<TPixel>.Instance.ToVector4(configuration, this.Palette.Span, this.vectorCache);
this.rgbaPalette = new Rgba32[palette.Length];
this.cache = new ColorDistanceCache(configuration.MemoryAllocator);
PixelOperations<TPixel>.Instance.ToRgba32(configuration, this.Palette.Span, this.rgbaPalette);
}
/// <summary>
@ -57,11 +59,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public int GetClosestColor(TPixel color, out TPixel match)
{
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.Palette.Span);
Unsafe.SkipInit(out Rgba32 rgba);
color.ToRgba32(ref rgba);
// Check if the color is in the lookup table
if (!this.distanceCache.TryGetValue(color, out int index))
if (!this.cache.TryGetValue(rgba, out short index))
{
return this.GetClosestColorSlow(color, ref paletteRef, out match);
return this.GetClosestColorSlow(rgba, ref paletteRef, out match);
}
match = Unsafe.Add(ref paletteRef, index);
@ -69,17 +73,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
}
[MethodImpl(InliningOptions.ShortMethod)]
private int GetClosestColorSlow(TPixel color, ref TPixel paletteRef, out TPixel match)
private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match)
{
// Loop through the palette and find the nearest match.
int index = 0;
float leastDistance = float.MaxValue;
var vector = color.ToVector4();
ref Vector4 vectorCacheRef = ref MemoryMarshal.GetReference<Vector4>(this.vectorCache);
for (int i = 0; i < this.Palette.Length; i++)
for (int i = 0; i < this.rgbaPalette.Length; i++)
{
Vector4 candidate = Unsafe.Add(ref vectorCacheRef, i);
float distance = Vector4.DistanceSquared(vector, candidate);
Rgba32 candidate = this.rgbaPalette[i];
int distance = DistanceSquared(rgba, candidate);
// If it's an exact match, exit the loop
if (distance == 0)
@ -97,9 +99,103 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
}
// Now I have the index, pop it into the cache for next time
this.distanceCache[color] = index;
this.cache.Add(rgba, (byte)index);
match = Unsafe.Add(ref paletteRef, index);
return index;
}
/// <summary>
/// Returns the Euclidean distance squared between two specified points.
/// </summary>
/// <param name="a">The first point.</param>
/// <param name="b">The second point.</param>
/// <returns>The distance squared.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
private static int DistanceSquared(Rgba32 a, Rgba32 b)
{
int deltaR = a.R - b.R;
int deltaG = a.G - b.G;
int deltaB = a.B - b.B;
int deltaA = a.A - b.A;
return (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB) + (deltaA * deltaA);
}
public void Dispose() => this.cache.Dispose();
/// <summary>
/// A cache for storing color distance matching results.
/// </summary>
/// <remarks>
/// <para>
/// The granularity of the cache has been determined based upon the current
/// suite of test images and provides the lowest possible memory usage while
/// providing enough match accuracy.
/// Entry count is currently limited to 1185921 entries (2371842 bytes ~2.26MB).
/// </para>
/// </remarks>
private unsafe struct ColorDistanceCache : IDisposable
{
private const int IndexBits = 5;
private const int IndexAlphaBits = 5;
private const int IndexCount = (1 << IndexBits) + 1;
private const int IndexAlphaCount = (1 << IndexAlphaBits) + 1;
private const int RgbShift = 8 - IndexBits;
private const int AlphaShift = 8 - IndexAlphaBits;
private const int Entries = IndexCount * IndexCount * IndexCount * IndexAlphaCount;
private MemoryHandle tableHandle;
private readonly IMemoryOwner<short> table;
private readonly short* tablePointer;
public ColorDistanceCache(MemoryAllocator allocator)
{
this.table = allocator.Allocate<short>(Entries);
this.table.GetSpan().Fill(-1);
this.tableHandle = this.table.Memory.Pin();
this.tablePointer = (short*)this.tableHandle.Pointer;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Add(Rgba32 rgba, byte index)
{
int r = rgba.R >> RgbShift;
int g = rgba.G >> RgbShift;
int b = rgba.B >> RgbShift;
int a = rgba.A >> AlphaShift;
int idx = GetPaletteIndex(r, g, b, a);
this.tablePointer[idx] = index;
}
[MethodImpl(InliningOptions.ShortMethod)]
public bool TryGetValue(Rgba32 rgba, out short match)
{
int r = rgba.R >> RgbShift;
int g = rgba.G >> RgbShift;
int b = rgba.B >> RgbShift;
int a = rgba.A >> AlphaShift;
int idx = GetPaletteIndex(r, g, b, a);
match = this.tablePointer[idx];
return match > -1;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static int GetPaletteIndex(int r, int g, int b, int a)
=> (r << ((IndexBits << 1) + IndexAlphaBits))
+ (r << (IndexBits + IndexAlphaBits + 1))
+ (g << (IndexBits + IndexAlphaBits))
+ (r << (IndexBits << 1))
+ (r << (IndexBits + 1))
+ (g << IndexBits)
+ ((r + g + b) << IndexAlphaBits)
+ r + g + b + a;
public void Dispose()
{
if (this.table != null)
{
this.tableHandle.Dispose();
this.table.Dispose();
}
}
}
}
}

98
src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs

@ -20,10 +20,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly int maxColors;
private readonly int bitDepth;
private readonly Octree octree;
private IMemoryOwner<TPixel> paletteOwner;
private ReadOnlyMemory<TPixel> palette;
private EuclideanPixelMap<TPixel> pixelMap;
private bool pixelMapHasValue;
private readonly bool isDithering;
private bool isDisposed;
@ -42,10 +44,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.Options = options;
this.maxColors = this.Options.MaxColors;
this.octree = new Octree(Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(this.maxColors), 1, 8));
this.bitDepth = Numerics.Clamp(ColorNumerics.GetBitsNeededForColorDepth(this.maxColors), 1, 8);
this.octree = new Octree(this.bitDepth);
this.paletteOwner = configuration.MemoryAllocator.Allocate<TPixel>(this.maxColors, AllocationOptions.Clean);
this.palette = default;
this.pixelMap = default;
this.pixelMapHasValue = false;
this.palette = default;
this.isDithering = !(this.Options.Dither is null);
this.isDisposed = false;
}
@ -67,37 +71,56 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void AddPaletteColors(Buffer2DRegion<TPixel> pixelRegion)
{
Rectangle bounds = pixelRegion.Rectangle;
Buffer2D<TPixel> source = pixelRegion.Buffer;
using IMemoryOwner<Rgba32> buffer = this.Configuration.MemoryAllocator.Allocate<Rgba32>(bounds.Width);
Span<Rgba32> bufferSpan = buffer.GetSpan();
// Loop through each row
for (int y = bounds.Top; y < bounds.Bottom; y++)
using (IMemoryOwner<Rgba32> buffer = this.Configuration.MemoryAllocator.Allocate<Rgba32>(bounds.Width))
{
Span<TPixel> row = source.GetRowSpan(y).Slice(bounds.Left, bounds.Width);
PixelOperations<TPixel>.Instance.ToRgba32(this.Configuration, row, bufferSpan);
Span<Rgba32> bufferSpan = buffer.GetSpan();
for (int x = 0; x < bufferSpan.Length; x++)
// Loop through each row
for (int y = bounds.Top; y < bounds.Bottom; y++)
{
Rgba32 rgba = bufferSpan[x];
Span<TPixel> row = source.GetRowSpan(y).Slice(bounds.Left, bounds.Width);
PixelOperations<TPixel>.Instance.ToRgba32(this.Configuration, row, bufferSpan);
for (int x = 0; x < bufferSpan.Length; x++)
{
Rgba32 rgba = bufferSpan[x];
// Add the color to the Octree
this.octree.AddColor(rgba);
// Add the color to the Octree
this.octree.AddColor(rgba);
}
}
}
Span<TPixel> paletteSpan = this.paletteOwner.GetSpan();
int paletteIndex = 0;
this.octree.Palletize(paletteSpan, this.maxColors, ref paletteIndex);
Span<TPixel> paletteSpan = this.paletteOwner.GetSpan();
// Length of reduced palette + transparency.
ReadOnlyMemory<TPixel> result = this.paletteOwner.Memory.Slice(0, Math.Min(paletteIndex + 2, this.maxColors));
this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, result);
// On very rare occasions, (blur.png), the quantizer does not preserve a
// transparent entry when palletizing the captured colors.
// To workaround this we ensure the palette ends with the default color
// for higher bit depths. Lower bit depths will correctly reduce the palette.
// TODO: Investigate more evenly reduced palette reduction.
int max = this.maxColors;
if (this.bitDepth == 8)
{
max--;
}
this.octree.Palletize(paletteSpan, max, ref paletteIndex);
ReadOnlyMemory<TPixel> result = this.paletteOwner.Memory.Slice(0, paletteSpan.Length);
// When called by QuantizerUtilities.BuildPalette this prevents
// mutiple instances of the map being created but not disposed.
if (this.pixelMapHasValue)
{
this.pixelMap.Dispose();
}
this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, result);
this.pixelMapHasValue = true;
this.palette = result;
}
@ -118,8 +141,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
return (byte)this.pixelMap.GetClosestColor(color, out match);
}
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.pixelMap.Palette.Span);
var index = (byte)this.octree.GetPaletteIndex(color);
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.palette.Span);
byte index = (byte)this.octree.GetPaletteIndex(color);
match = Unsafe.Add(ref paletteRef, index);
return index;
}
@ -132,6 +155,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.isDisposed = true;
this.paletteOwner.Dispose();
this.paletteOwner = null;
this.pixelMap.Dispose();
}
}
@ -176,21 +200,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.previousNode = null;
}
/// <summary>
/// Gets the mask used when getting the appropriate pixels for a given node.
/// </summary>
private static ReadOnlySpan<byte> Mask => new byte[]
{
0b10000000,
0b1000000,
0b100000,
0b10000,
0b1000,
0b100,
0b10,
0b1
};
/// <summary>
/// Gets or sets the number of leaves in the tree
/// </summary>
@ -251,7 +260,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
[MethodImpl(InliningOptions.ShortMethod)]
public void Palletize(Span<TPixel> palette, int colorCount, ref int paletteIndex)
{
while (this.Leaves > colorCount - 1)
while (this.Leaves > colorCount)
{
this.Reduce();
}
@ -269,7 +278,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
[MethodImpl(InliningOptions.ShortMethod)]
public int GetPaletteIndex(TPixel color)
{
Rgba32 rgba = default;
Unsafe.SkipInit(out Rgba32 rgba);
color.ToRgba32(ref rgba);
return this.root.GetPaletteIndex(ref rgba, 0);
}
@ -468,7 +477,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
Vector3.Zero,
new Vector3(255));
TPixel pixel = default;
Unsafe.SkipInit(out TPixel pixel);
pixel.FromRgba32(new Rgba32((byte)vector.X, (byte)vector.Y, (byte)vector.Z, byte.MaxValue));
palette[index] = pixel;
@ -517,7 +526,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
child = this.children[i];
if (child != null)
{
var childIndex = child.GetPaletteIndex(ref pixel, level + 1);
int childIndex = child.GetPaletteIndex(ref pixel, level + 1);
if (childIndex != 0)
{
return childIndex;
@ -538,15 +547,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
[MethodImpl(InliningOptions.ShortMethod)]
private static int GetColorIndex(ref Rgba32 color, int level)
{
DebugGuard.MustBeLessThan(level, Mask.Length, nameof(level));
int shift = 7 - level;
ref byte maskRef = ref MemoryMarshal.GetReference(Mask);
byte mask = Unsafe.Add(ref maskRef, level);
byte mask = (byte)(1 << shift);
return ((color.R & mask) >> shift)
| ((color.G & mask) >> (shift - 1))
| ((color.B & mask) >> (shift - 2));
| (((color.G & mask) >> shift) << 1)
| (((color.B & mask) >> shift) << 2);
}
/// <summary>

2
src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs

@ -60,7 +60,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
Color.ToPixel(configuration, this.colorPalette.Span, palette.AsSpan());
var pixelMap = new EuclideanPixelMap<TPixel>(configuration, palette);
return new PaletteQuantizer<TPixel>(configuration, options, pixelMap);
return new PaletteQuantizer<TPixel>(configuration, options, pixelMap, false);
}
}
}

12
src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs

@ -17,6 +17,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly EuclideanPixelMap<TPixel> pixelMap;
private readonly bool leaveMap;
/// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer{TPixel}"/> struct.
@ -24,11 +25,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="options">The quantizer options defining quantization rules.</param>
/// <param name="pixelMap">The pixel map for looking up color matches from a predefined palette.</param>
/// <param name="leaveMap">
/// <see langword="true"/> to leave the pixel map undisposed after disposing the <see cref="PaletteQuantizer{TPixel}"/> object; otherwise, <see langword="false"/>.
/// </param>
[MethodImpl(InliningOptions.ShortMethod)]
public PaletteQuantizer(
Configuration configuration,
QuantizerOptions options,
EuclideanPixelMap<TPixel> pixelMap)
EuclideanPixelMap<TPixel> pixelMap,
bool leaveMap)
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(options, nameof(options));
@ -36,6 +41,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.Configuration = configuration;
this.Options = options;
this.pixelMap = pixelMap;
this.leaveMap = leaveMap;
}
/// <inheritdoc/>
@ -66,6 +72,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <inheritdoc/>
public void Dispose()
{
if (!this.leaveMap)
{
this.pixelMap.Dispose();
}
}
}
}

44
src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs

@ -41,46 +41,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(source, interest);
var operation = new RowIntervalOperation(this.SourceRectangle, source, quantized);
ParallelRowIterator.IterateRowIntervals(
configuration,
interest,
in operation);
}
private readonly struct RowIntervalOperation : IRowIntervalOperation
{
private readonly Rectangle bounds;
private readonly ImageFrame<TPixel> source;
private readonly IndexedImageFrame<TPixel> quantized;
ReadOnlySpan<TPixel> paletteSpan = quantized.Palette.Span;
int offsetY = interest.Top;
int offsetX = interest.Left;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
Rectangle bounds,
ImageFrame<TPixel> source,
IndexedImageFrame<TPixel> quantized)
for (int y = interest.Y; y < interest.Height; y++)
{
this.bounds = bounds;
this.source = source;
this.quantized = quantized;
}
Span<TPixel> row = source.GetPixelRowSpan(y);
ReadOnlySpan<byte> quantizedRow = quantized.GetPixelRowSpan(y - offsetY);
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
ReadOnlySpan<TPixel> paletteSpan = this.quantized.Palette.Span;
int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left;
for (int y = rows.Min; y < rows.Max; y++)
for (int x = interest.Left; x < interest.Right; x++)
{
Span<TPixel> row = this.source.GetPixelRowSpan(y);
ReadOnlySpan<byte> quantizedRow = this.quantized.GetPixelRowSpan(y - offsetY);
for (int x = this.bounds.Left; x < this.bounds.Right; x++)
{
row[x] = paletteSpan[quantizedRow[x - offsetX]];
}
row[x] = paletteSpan[quantizedRow[x - offsetX]];
}
}
}

61
src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs

@ -3,7 +3,6 @@
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Dithering;
@ -126,62 +125,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
if (dither is null)
{
var operation = new RowIntervalOperation<TFrameQuantizer, TPixel>(
ref quantizer,
source,
destination,
bounds);
int offsetY = bounds.Top;
int offsetX = bounds.Left;
ParallelRowIterator.IterateRowIntervals(
quantizer.Configuration,
bounds,
in operation);
return;
}
dither.ApplyQuantizationDither(ref quantizer, source, destination, bounds);
}
private readonly struct RowIntervalOperation<TFrameQuantizer, TPixel> : IRowIntervalOperation
where TFrameQuantizer : struct, IQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly TFrameQuantizer quantizer;
private readonly ImageFrame<TPixel> source;
private readonly IndexedImageFrame<TPixel> destination;
private readonly Rectangle bounds;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
ref TFrameQuantizer quantizer,
ImageFrame<TPixel> source,
IndexedImageFrame<TPixel> destination,
Rectangle bounds)
{
this.quantizer = quantizer;
this.source = source;
this.destination = destination;
this.bounds = bounds;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
int offsetY = this.bounds.Top;
int offsetX = this.bounds.Left;
for (int y = rows.Min; y < rows.Max; y++)
for (int y = bounds.Y; y < bounds.Height; y++)
{
Span<TPixel> sourceRow = this.source.GetPixelRowSpan(y);
Span<byte> destinationRow = this.destination.GetWritablePixelRowSpanUnsafe(y - offsetY);
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<byte> destinationRow = destination.GetWritablePixelRowSpanUnsafe(y - offsetY);
for (int x = this.bounds.Left; x < this.bounds.Right; x++)
for (int x = bounds.Left; x < bounds.Right; x++)
{
destinationRow[x - offsetX] = Unsafe.AsRef(this.quantizer).GetQuantizedColor(sourceRow[x], out TPixel _);
destinationRow[x - offsetX] = Unsafe.AsRef(quantizer).GetQuantizedColor(sourceRow[x], out TPixel _);
}
}
return;
}
dither.ApplyQuantizationDither(ref quantizer, source, destination, bounds);
}
}
}

29
src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs

@ -6,7 +6,6 @@ using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -73,6 +72,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
private int maxColors;
private readonly Box[] colorCube;
private EuclideanPixelMap<TPixel> pixelMap;
private bool pixelMapHasValue;
private readonly bool isDithering;
private bool isDisposed;
@ -94,10 +94,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.momentsOwner = this.memoryAllocator.Allocate<Moment>(TableLength, AllocationOptions.Clean);
this.tagsOwner = this.memoryAllocator.Allocate<byte>(TableLength, AllocationOptions.Clean);
this.paletteOwner = this.memoryAllocator.Allocate<TPixel>(this.maxColors, AllocationOptions.Clean);
this.palette = default;
this.colorCube = new Box[this.maxColors];
this.isDisposed = false;
this.pixelMap = default;
this.pixelMapHasValue = false;
this.palette = default;
this.isDithering = this.isDithering = !(this.Options.Dither is null);
}
@ -127,9 +128,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.Get3DMoments(this.memoryAllocator);
this.BuildCube();
// Slice again since maxColors has been updated since the buffer was created.
Span<TPixel> paletteSpan = this.paletteOwner.GetSpan().Slice(0, this.maxColors);
ReadOnlySpan<Moment> momentsSpan = this.momentsOwner.GetSpan();
Span<TPixel> paletteSpan = this.paletteOwner.GetSpan();
for (int k = 0; k < this.maxColors; k++)
for (int k = 0; k < paletteSpan.Length; k++)
{
this.Mark(ref this.colorCube[k], (byte)k);
@ -142,8 +144,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
}
}
ReadOnlyMemory<TPixel> result = this.paletteOwner.Memory.Slice(0, this.maxColors);
this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, result);
ReadOnlyMemory<TPixel> result = this.paletteOwner.Memory.Slice(0, paletteSpan.Length);
if (this.isDithering)
{
// When called by QuantizerUtilities.BuildPalette this prevents
// mutiple instances of the map being created but not disposed.
if (this.pixelMapHasValue)
{
this.pixelMap.Dispose();
}
this.pixelMap = new EuclideanPixelMap<TPixel>(this.Configuration, result);
this.pixelMapHasValue = true;
}
this.palette = result;
}
@ -170,7 +184,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
ReadOnlySpan<byte> tagSpan = this.tagsOwner.GetSpan();
byte index = tagSpan[GetPaletteIndex(r + 1, g + 1, b + 1, a + 1)];
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.pixelMap.Palette.Span);
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.palette.Span);
match = Unsafe.Add(ref paletteRef, index);
return index;
}
@ -187,6 +201,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
this.momentsOwner = null;
this.tagsOwner = null;
this.paletteOwner = null;
this.pixelMap.Dispose();
}
}

4
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs

@ -130,9 +130,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int radius = (int)TolerantMath.Ceiling(scale * sampler.Radius);
// 'ratio' is a rational number.
// Multiplying it by LCM(sourceSize, destSize)/sourceSize will result in a whole number "again".
// Multiplying it by destSize/GCD(sourceSize, destSize) will result in a whole number "again".
// This value is determining the length of the periods in repeating kernel map rows.
int period = Numerics.LeastCommonMultiple(sourceSize, destinationSize) / sourceSize;
int period = destinationSize / Numerics.GreatestCommonDivisor(sourceSize, destinationSize);
// the center position at i == 0:
double center0 = (ratio - 1) * 0.5;

4
tests/Directory.Build.targets

@ -22,8 +22,8 @@
<PackageReference Update="BenchmarkDotNet.Diagnostics.Windows" Version="0.12.1" Condition="'$(IsWindows)'=='true'" />
<PackageReference Update="Colourful" Version="2.0.5" />
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="7.23.2.1" />
<PackageReference Update="Microsoft.DotNet.RemoteExecutor" Version="6.0.0-beta.20513.1" />
<PackageReference Update="Microsoft.DotNet.XUnitExtensions" Version="6.0.0-beta.20513.1" />
<PackageReference Update="Microsoft.DotNet.RemoteExecutor" Version="6.0.0-beta.21311.3" />
<PackageReference Update="Microsoft.DotNet.XUnitExtensions" Version="6.0.0-beta.21311.3" />
<PackageReference Update="Moq" Version="4.14.6" />
<PackageReference Update="Pfim" Version="0.9.1" />
<PackageReference Include="runtime.osx.10.10-x64.CoreCompat.System.Drawing" Version="5.8.64" Condition="'$(IsOSX)'=='true'" />

38
tests/ImageSharp.Benchmarks/Codecs/Jpeg/BlockOperations/Block8x8F_Scale16X16To8X8.cs

@ -1,38 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
namespace SixLabors.ImageSharp.Benchmarks.Format.Jpeg.Components
{
[Config(typeof(Config.HwIntrinsics_SSE_AVX))]
public class Block8x8F_Scale16X16To8X8
{
private Block8x8F source;
private readonly Block8x8F[] target = new Block8x8F[4];
[GlobalSetup]
public void Setup()
{
var random = new Random();
float[] f = new float[8 * 8];
for (int i = 0; i < f.Length; i++)
{
f[i] = (float)random.NextDouble();
}
for (int i = 0; i < 4; i++)
{
this.target[i] = Block8x8F.Load(f);
}
this.source = Block8x8F.Load(f);
}
[Benchmark]
public void Scale16X16To8X8() => Block8x8F.Scale16X16To8X8(ref this.source, this.target);
}
}

95
tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpeg.cs

@ -4,6 +4,7 @@
using System.Drawing.Imaging;
using System.IO;
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests;
using SDImage = System.Drawing.Image;
@ -12,10 +13,22 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{
public class EncodeJpeg
{
// System.Drawing needs this.
private Stream bmpStream;
[Params(75, 90, 100)]
public int Quality;
private const string TestImage = TestImages.Jpeg.BenchmarkSuite.Jpeg420Exif_MidSizeYCbCr;
// System.Drawing
private SDImage bmpDrawing;
private Stream bmpStream;
private ImageCodecInfo jpegCodec;
private EncoderParameters encoderParameters;
// ImageSharp
private Image<Rgba32> bmpCore;
private JpegEncoder encoder420;
private JpegEncoder encoder444;
private MemoryStream destinationStream;
[GlobalSetup]
@ -23,12 +36,23 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{
if (this.bmpStream == null)
{
const string TestImage = TestImages.Jpeg.BenchmarkSuite.Jpeg420Exif_MidSizeYCbCr;
this.bmpStream = File.OpenRead(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImage));
this.bmpCore = Image.Load<Rgba32>(this.bmpStream);
this.bmpCore.Metadata.ExifProfile = null;
this.encoder420 = new JpegEncoder { Quality = this.Quality, Subsample = JpegSubsample.Ratio420 };
this.encoder444 = new JpegEncoder { Quality = this.Quality, Subsample = JpegSubsample.Ratio444 };
this.bmpStream.Position = 0;
this.bmpDrawing = SDImage.FromStream(this.bmpStream);
this.jpegCodec = GetEncoder(ImageFormat.Jpeg);
this.encoderParameters = new EncoderParameters(1);
// Quality cast to long is necessary
#pragma warning disable IDE0004 // Remove Unnecessary Cast
this.encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, (long)this.Quality);
#pragma warning restore IDE0004 // Remove Unnecessary Cast
this.destinationStream = new MemoryStream();
}
}
@ -38,36 +62,73 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg
{
this.bmpStream.Dispose();
this.bmpStream = null;
this.destinationStream.Dispose();
this.destinationStream = null;
this.bmpCore.Dispose();
this.bmpDrawing.Dispose();
this.encoderParameters.Dispose();
}
[Benchmark(Baseline = true, Description = "System.Drawing Jpeg")]
[Benchmark(Baseline = true, Description = "System.Drawing Jpeg 4:2:0")]
public void JpegSystemDrawing()
{
this.bmpDrawing.Save(this.destinationStream, ImageFormat.Jpeg);
this.bmpDrawing.Save(this.destinationStream, this.jpegCodec, this.encoderParameters);
this.destinationStream.Seek(0, SeekOrigin.Begin);
}
[Benchmark(Description = "ImageSharp Jpeg 4:2:0")]
public void JpegCore420()
{
this.bmpCore.SaveAsJpeg(this.destinationStream, this.encoder420);
this.destinationStream.Seek(0, SeekOrigin.Begin);
}
[Benchmark(Description = "ImageSharp Jpeg")]
public void JpegCore()
[Benchmark(Description = "ImageSharp Jpeg 4:4:4")]
public void JpegCore444()
{
this.bmpCore.SaveAsJpeg(this.destinationStream);
this.bmpCore.SaveAsJpeg(this.destinationStream, this.encoder444);
this.destinationStream.Seek(0, SeekOrigin.Begin);
}
// https://docs.microsoft.com/en-us/dotnet/api/system.drawing.imaging.encoderparameter?redirectedfrom=MSDN&view=net-5.0
private static ImageCodecInfo GetEncoder(ImageFormat format)
{
ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();
foreach (ImageCodecInfo codec in codecs)
{
if (codec.FormatID == format.Guid)
{
return codec;
}
}
return null;
}
}
}
/*
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.959 (1909/November2018Update/19H2)
Intel Core i7-8650U CPU 1.90GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.302
[Host] : .NET Core 3.1.6 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.31603), X64 RyuJIT
DefaultJob : .NET Core 3.1.6 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.31603), X64 RyuJIT
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=6.0.100-preview.3.21202.5
[Host] : .NET Core 3.1.13 (CoreCLR 4.700.21.11102, CoreFX 4.700.21.11602), X64 RyuJIT [AttachedDebugger]
DefaultJob : .NET Core 3.1.13 (CoreCLR 4.700.21.11102, CoreFX 4.700.21.11602), X64 RyuJIT
| Method | Mean | Error | StdDev | Ratio | RatioSD |
|---------------------- |---------:|----------:|----------:|------:|--------:|
| 'System.Drawing Jpeg' | 4.297 ms | 0.0244 ms | 0.0228 ms | 1.00 | 0.00 |
| 'ImageSharp Jpeg' | 5.286 ms | 0.1034 ms | 0.0967 ms | 1.23 | 0.02 |
| Method | Quality | Mean | Error | StdDev | Ratio | RatioSD |
|---------------------------- |-------- |---------:|---------:|---------:|------:|--------:|
| 'System.Drawing Jpeg 4:2:0' | 75 | 30.60 ms | 0.496 ms | 0.464 ms | 1.00 | 0.00 |
| 'ImageSharp Jpeg 4:2:0' | 75 | 29.86 ms | 0.350 ms | 0.311 ms | 0.98 | 0.02 |
| 'ImageSharp Jpeg 4:4:4' | 75 | 45.36 ms | 0.899 ms | 1.036 ms | 1.48 | 0.05 |
| | | | | | | |
| 'System.Drawing Jpeg 4:2:0' | 90 | 34.05 ms | 0.669 ms | 0.687 ms | 1.00 | 0.00 |
| 'ImageSharp Jpeg 4:2:0' | 90 | 37.26 ms | 0.706 ms | 0.660 ms | 1.10 | 0.03 |
| 'ImageSharp Jpeg 4:4:4' | 90 | 52.54 ms | 0.579 ms | 0.514 ms | 1.55 | 0.04 |
| | | | | | | |
| 'System.Drawing Jpeg 4:2:0' | 100 | 39.36 ms | 0.267 ms | 0.237 ms | 1.00 | 0.00 |
| 'ImageSharp Jpeg 4:2:0' | 100 | 42.44 ms | 0.410 ms | 0.383 ms | 1.08 | 0.01 |
| 'ImageSharp Jpeg 4:4:4' | 100 | 70.88 ms | 0.508 ms | 0.450 ms | 1.80 | 0.02 |
*/

4
tests/ImageSharp.Benchmarks/Format/Jpeg/Components/Encoder/YCbCrForwardConverterBenchmark.cs

@ -37,7 +37,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Format.Jpeg.Components.Encoder
Block8x8F cb = default;
Block8x8F cr = default;
this.converter.Convert(this.data.AsSpan(), ref y, ref cb, ref cr);
this.converter.Convert444(this.data.AsSpan(), ref y, ref cb, ref cr);
}
[Benchmark]
@ -49,7 +49,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Format.Jpeg.Components.Encoder
if (RgbToYCbCrConverterVectorized.IsSupported)
{
RgbToYCbCrConverterVectorized.Convert(this.data.AsSpan(), ref y, ref cb, ref cr);
RgbToYCbCrConverterVectorized.Convert444(this.data.AsSpan(), ref y, ref cb, ref cr);
}
}
}

2
tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj

@ -8,7 +8,7 @@
<GenerateProgramFile>false</GenerateProgramFile>
<!--Used to hide test project from dotnet test-->
<IsTestProject>false</IsTestProject>
<Configurations>Debug;Release;Release-InnerLoop;Debug-InnerLoop</Configurations>
<Configurations>Debug;Release;Debug-InnerLoop;Release-InnerLoop</Configurations>
<!-- Uncomment this to run benchmarks depending on Colorful or Pfim (colorspaces and TGA): -->
<!--<SignAssembly>false</SignAssembly>-->

2
tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj

@ -12,7 +12,7 @@
<!--Used to hide test project from dotnet test-->
<IsTestProject>false</IsTestProject>
<EnsureNETCoreAppRuntime>false</EnsureNETCoreAppRuntime>
<Configurations>Debug;Release;Release-InnerLoop;Debug-InnerLoop</Configurations>
<Configurations>Debug;Release;Debug-InnerLoop;Release-InnerLoop</Configurations>
<ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
</PropertyGroup>

73
tests/ImageSharp.Tests/Common/NumericsTests.cs

@ -0,0 +1,73 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using Xunit;
using Xunit.Abstractions;
namespace SixLabors.ImageSharp.Tests.Common
{
public class NumericsTests
{
private ITestOutputHelper Output { get; }
public NumericsTests(ITestOutputHelper output)
{
this.Output = output;
}
private static int Log2_ReferenceImplementation(uint value)
{
int n = 0;
while ((value >>= 1) != 0)
{
++n;
}
return n;
}
[Fact]
public void Log2_ZeroConvention()
{
uint value = 0;
int expected = 0;
int actual = Numerics.Log2(value);
Assert.True(expected == actual, $"Expected: {expected}, Actual: {actual}");
}
[Fact]
public void Log2_PowersOfTwo()
{
for (int i = 0; i < sizeof(int) * 8; i++)
{
// from 2^0 to 2^32
uint value = (uint)(1 << i);
int expected = i;
int actual = Numerics.Log2(value);
Assert.True(expected == actual, $"Expected: {expected}, Actual: {actual}");
}
}
[Theory]
[InlineData(1, 100)]
[InlineData(2, 100)]
public void Log2_RandomValues(int seed, int count)
{
var rng = new Random(seed);
byte[] bytes = new byte[4];
for (int i = 0; i < count; i++)
{
rng.NextBytes(bytes);
uint value = BitConverter.ToUInt32(bytes, 0);
int expected = Log2_ReferenceImplementation(value);
int actual = Numerics.Log2(value);
Assert.True(expected == actual, $"Expected: {expected}, Actual: {actual}");
}
}
}
}

2
tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs

@ -343,7 +343,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp
{
BitsPerPixel = bitsPerPixel,
SupportTransparency = supportTransparency,
Quantizer = quantizer ?? KnownQuantizers.Wu
Quantizer = quantizer ?? KnownQuantizers.Octree
};
// Does DebugSave & load reference CompareToReferenceInput():

264
tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs

@ -2,10 +2,12 @@
// Licensed under the Apache License, Version 2.0.
using System;
#if SUPPORTS_RUNTIME_INTRINSICS
using System.Runtime.Intrinsics.X86;
#endif
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils;
using SixLabors.ImageSharp.Tests.TestUtilities;
using Xunit;
using Xunit.Abstractions;
@ -22,94 +24,180 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
}
[Fact]
public void IDCT2D8x4_LeftPart()
// Reference tests
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public void LLM_TransformIDCT_CompareToNonOptimized(int seed)
{
float[] sourceArray = Create8x8FloatData();
var expectedDestArray = new float[64];
float[] sourceArray = Create8x8RoundedRandomFloatData(-1000, 1000, seed);
var source = Block8x8F.Load(sourceArray);
ReferenceImplementations.LLM_FloatingPoint_DCT.IDCT2D8x4_32f(sourceArray, expectedDestArray);
Block8x8F expected = ReferenceImplementations.LLM_FloatingPoint_DCT.TransformIDCT(ref source);
var temp = default(Block8x8F);
var actual = default(Block8x8F);
FastFloatingPointDCT.TransformIDCT(ref source, ref actual, ref temp);
var source = default(Block8x8F);
source.LoadFrom(sourceArray);
this.CompareBlocks(expected, actual, 1f);
}
var dest = default(Block8x8F);
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public void LLM_TransformIDCT_CompareToAccurate(int seed)
{
float[] sourceArray = Create8x8RoundedRandomFloatData(-1000, 1000, seed);
FastFloatingPointDCT.IDCT8x4_LeftPart(ref source, ref dest);
var source = Block8x8F.Load(sourceArray);
var actualDestArray = new float[64];
dest.ScaledCopyTo(actualDestArray);
Block8x8F expected = ReferenceImplementations.AccurateDCT.TransformIDCT(ref source);
this.Print8x8Data(expectedDestArray);
this.Output.WriteLine("**************");
this.Print8x8Data(actualDestArray);
var temp = default(Block8x8F);
var actual = default(Block8x8F);
FastFloatingPointDCT.TransformIDCT(ref source, ref actual, ref temp);
Assert.Equal(expectedDestArray, actualDestArray);
this.CompareBlocks(expected, actual, 1f);
}
[Fact]
public void IDCT2D8x4_RightPart()
// Inverse transform
[Theory]
[InlineData(1)]
[InlineData(2)]
public void IDCT8x4_LeftPart(int seed)
{
float[] sourceArray = Create8x8FloatData();
var expectedDestArray = new float[64];
ReferenceImplementations.LLM_FloatingPoint_DCT.IDCT2D8x4_32f(sourceArray.AsSpan(4), expectedDestArray.AsSpan(4));
Span<float> src = Create8x8RoundedRandomFloatData(-200, 200, seed);
var srcBlock = default(Block8x8F);
srcBlock.LoadFrom(src);
var source = default(Block8x8F);
source.LoadFrom(sourceArray);
var destBlock = default(Block8x8F);
var dest = default(Block8x8F);
var expectedDest = new float[64];
FastFloatingPointDCT.IDCT8x4_RightPart(ref source, ref dest);
// reference
ReferenceImplementations.LLM_FloatingPoint_DCT.IDCT2D8x4_32f(src, expectedDest);
var actualDestArray = new float[64];
dest.ScaledCopyTo(actualDestArray);
// testee
FastFloatingPointDCT.IDCT8x4_LeftPart(ref srcBlock, ref destBlock);
this.Print8x8Data(expectedDestArray);
this.Output.WriteLine("**************");
this.Print8x8Data(actualDestArray);
var actualDest = new float[64];
destBlock.ScaledCopyTo(actualDest);
Assert.Equal(expectedDestArray, actualDestArray);
Assert.Equal(actualDest, expectedDest, new ApproximateFloatComparer(1f));
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public void LLM_TransformIDCT_CompareToNonOptimized(int seed)
public void IDCT8x4_RightPart(int seed)
{
float[] sourceArray = Create8x8RoundedRandomFloatData(-1000, 1000, seed);
Span<float> src = Create8x8RoundedRandomFloatData(-200, 200, seed);
var srcBlock = default(Block8x8F);
srcBlock.LoadFrom(src);
var source = Block8x8F.Load(sourceArray);
var destBlock = default(Block8x8F);
Block8x8F expected = ReferenceImplementations.LLM_FloatingPoint_DCT.TransformIDCT(ref source);
var expectedDest = new float[64];
var temp = default(Block8x8F);
var actual = default(Block8x8F);
FastFloatingPointDCT.TransformIDCT(ref source, ref actual, ref temp);
// reference
ReferenceImplementations.LLM_FloatingPoint_DCT.IDCT2D8x4_32f(src.Slice(4), expectedDest.AsSpan(4));
this.CompareBlocks(expected, actual, 1f);
// testee
FastFloatingPointDCT.IDCT8x4_RightPart(ref srcBlock, ref destBlock);
var actualDest = new float[64];
destBlock.ScaledCopyTo(actualDest);
Assert.Equal(actualDest, expectedDest, new ApproximateFloatComparer(1f));
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public void LLM_TransformIDCT_CompareToAccurate(int seed)
public void IDCT8x8_Avx(int seed)
{
float[] sourceArray = Create8x8RoundedRandomFloatData(-1000, 1000, seed);
#if SUPPORTS_RUNTIME_INTRINSICS
var skip = !Avx.IsSupported;
#else
var skip = true;
#endif
if (skip)
{
this.Output.WriteLine("No AVX present, skipping test!");
return;
}
var source = Block8x8F.Load(sourceArray);
Span<float> src = Create8x8RoundedRandomFloatData(-200, 200, seed);
var srcBlock = default(Block8x8F);
srcBlock.LoadFrom(src);
Block8x8F expected = ReferenceImplementations.AccurateDCT.TransformIDCT(ref source);
var destBlock = default(Block8x8F);
var temp = default(Block8x8F);
var actual = default(Block8x8F);
FastFloatingPointDCT.TransformIDCT(ref source, ref actual, ref temp);
var expectedDest = new float[64];
this.CompareBlocks(expected, actual, 1f);
// reference, left part
ReferenceImplementations.LLM_FloatingPoint_DCT.IDCT2D8x4_32f(src, expectedDest);
// reference, right part
ReferenceImplementations.LLM_FloatingPoint_DCT.IDCT2D8x4_32f(src.Slice(4), expectedDest.AsSpan(4));
// testee, whole 8x8
FastFloatingPointDCT.IDCT8x8_Avx(ref srcBlock, ref destBlock);
var actualDest = new float[64];
destBlock.ScaledCopyTo(actualDest);
Assert.Equal(actualDest, expectedDest, new ApproximateFloatComparer(1f));
}
[Theory]
[InlineData(1)]
[InlineData(2)]
public void TransformIDCT(int seed)
{
static void RunTest(string serialized)
{
int seed = FeatureTestRunner.Deserialize<int>(serialized);
Span<float> src = Create8x8RoundedRandomFloatData(-200, 200, seed);
var srcBlock = default(Block8x8F);
srcBlock.LoadFrom(src);
var destBlock = default(Block8x8F);
var expectedDest = new float[64];
var temp1 = new float[64];
var temp2 = default(Block8x8F);
// reference
ReferenceImplementations.LLM_FloatingPoint_DCT.IDCT2D_llm(src, expectedDest, temp1);
// testee
FastFloatingPointDCT.TransformIDCT(ref srcBlock, ref destBlock, ref temp2);
var actualDest = new float[64];
destBlock.ScaledCopyTo(actualDest);
Assert.Equal(actualDest, expectedDest, new ApproximateFloatComparer(1f));
}
// 3 paths:
// 1. AllowAll - call avx/fma implementation
// 2. DisableFMA - call avx implementation without fma acceleration
// 3. DisableAvx - call fallback code of Vector4 implementation
//
// DisableSSE isn't needed because fallback Vector4 code will compile to either sse or fallback code with same result
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
seed,
HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX);
}
// Forward transform
[Theory]
[InlineData(1)]
[InlineData(2)]
@ -123,7 +211,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
var expectedDest = new float[64];
// reference
ReferenceImplementations.LLM_FloatingPoint_DCT.FDCT2D8x4_32f(src, expectedDest);
// testee
FastFloatingPointDCT.FDCT8x4_LeftPart(ref srcBlock, ref destBlock);
var actualDest = new float[64];
@ -145,7 +236,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
var expectedDest = new float[64];
// reference
ReferenceImplementations.LLM_FloatingPoint_DCT.FDCT2D8x4_32f(src.Slice(4), expectedDest.AsSpan(4));
// testee
FastFloatingPointDCT.FDCT8x4_RightPart(ref srcBlock, ref destBlock);
var actualDest = new float[64];
@ -157,8 +251,19 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Theory]
[InlineData(1)]
[InlineData(2)]
public void TransformFDCT(int seed)
public void FDCT8x8_Avx(int seed)
{
#if SUPPORTS_RUNTIME_INTRINSICS
var skip = !Avx.IsSupported;
#else
var skip = true;
#endif
if (skip)
{
this.Output.WriteLine("No AVX present, skipping test!");
return;
}
Span<float> src = Create8x8RoundedRandomFloatData(-200, 200, seed);
var srcBlock = default(Block8x8F);
srcBlock.LoadFrom(src);
@ -166,17 +271,64 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
var destBlock = default(Block8x8F);
var expectedDest = new float[64];
var temp1 = new float[64];
var temp2 = default(Block8x8F);
ReferenceImplementations.LLM_FloatingPoint_DCT.FDCT2D_llm(src, expectedDest, temp1, downscaleBy8: true);
FastFloatingPointDCT.TransformFDCT(ref srcBlock, ref destBlock, ref temp2, false);
// reference, left part
ReferenceImplementations.LLM_FloatingPoint_DCT.FDCT2D8x4_32f(src, expectedDest);
// reference, right part
ReferenceImplementations.LLM_FloatingPoint_DCT.FDCT2D8x4_32f(src.Slice(4), expectedDest.AsSpan(4));
// testee, whole 8x8
FastFloatingPointDCT.FDCT8x8_Avx(ref srcBlock, ref destBlock);
var actualDest = new float[64];
destBlock.ScaledCopyTo(actualDest);
Assert.Equal(actualDest, expectedDest, new ApproximateFloatComparer(1f));
}
[Theory]
[InlineData(1)]
[InlineData(2)]
public void TransformFDCT(int seed)
{
static void RunTest(string serialized)
{
int seed = FeatureTestRunner.Deserialize<int>(serialized);
Span<float> src = Create8x8RoundedRandomFloatData(-200, 200, seed);
var srcBlock = default(Block8x8F);
srcBlock.LoadFrom(src);
var destBlock = default(Block8x8F);
var expectedDest = new float[64];
var temp1 = new float[64];
var temp2 = default(Block8x8F);
// reference
ReferenceImplementations.LLM_FloatingPoint_DCT.FDCT2D_llm(src, expectedDest, temp1, downscaleBy8: true);
// testee
FastFloatingPointDCT.TransformFDCT(ref srcBlock, ref destBlock, ref temp2, false);
var actualDest = new float[64];
destBlock.ScaledCopyTo(actualDest);
Assert.Equal(actualDest, expectedDest, new ApproximateFloatComparer(1f));
}
// 3 paths:
// 1. AllowAll - call avx/fma implementation
// 2. DisableFMA - call avx implementation without fma acceleration
// 3. DisableAvx - call fallback code of Vector4 implementation
//
// DisableSSE isn't needed because fallback Vector4 code will compile to either sse or fallback code with same result
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
seed,
HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX);
}
}
}
}

213
tests/ImageSharp.Tests/Formats/Jpg/RgbToYCbCrConverterTests.cs

@ -1,7 +1,13 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
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.ColorSpaces;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder;
@ -23,22 +29,23 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
private ITestOutputHelper Output { get; }
[Fact]
public void TestLutConverter()
public void TestConverterLut444()
{
Rgb24[] data = CreateTestData();
int dataSize = 8 * 8;
Rgb24[] data = CreateTestData(dataSize);
var target = RgbToYCbCrConverterLut.Create();
Block8x8F y = default;
Block8x8F cb = default;
Block8x8F cr = default;
target.Convert(data.AsSpan(), ref y, ref cb, ref cr);
target.Convert444(data.AsSpan(), ref y, ref cb, ref cr);
Verify(data, ref y, ref cb, ref cr, new ApproximateColorSpaceComparer(1F));
Verify444(data, ref y, ref cb, ref cr, new ApproximateColorSpaceComparer(1F));
}
[Fact]
public void TestVectorizedConverter()
public void TestConverterVectorized444()
{
if (!RgbToYCbCrConverterVectorized.IsSupported)
{
@ -46,18 +53,186 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
return;
}
Rgb24[] data = CreateTestData();
int dataSize = 8 * 8;
Rgb24[] data = CreateTestData(dataSize);
Block8x8F y = default;
Block8x8F cb = default;
Block8x8F cr = default;
RgbToYCbCrConverterVectorized.Convert(data.AsSpan(), ref y, ref cb, ref cr);
RgbToYCbCrConverterVectorized.Convert444(data.AsSpan(), ref y, ref cb, ref cr);
Verify(data, ref y, ref cb, ref cr, new ApproximateColorSpaceComparer(0.0001F));
Verify444(data, ref y, ref cb, ref cr, new ApproximateColorSpaceComparer(0.0001F));
}
private static void Verify(ReadOnlySpan<Rgb24> data, ref Block8x8F yResult, ref Block8x8F cbResult, ref Block8x8F crResult, ApproximateColorSpaceComparer comparer)
[Fact]
public void TestConverterLut420()
{
int dataSize = 16 * 16;
Span<Rgb24> data = CreateTestData(dataSize).AsSpan();
var target = RgbToYCbCrConverterLut.Create();
var yBlocks = new Block8x8F[4];
var cb = default(Block8x8F);
var cr = default(Block8x8F);
target.Convert420(data, ref yBlocks[0], ref yBlocks[1], ref cb, ref cr, 0);
target.Convert420(data.Slice(16 * 8), ref yBlocks[2], ref yBlocks[3], ref cb, ref cr, 1);
Verify420(data, yBlocks, ref cb, ref cr, new ApproximateFloatComparer(1F));
}
[Fact]
public void TestConverterVectorized420()
{
if (!RgbToYCbCrConverterVectorized.IsSupported)
{
this.Output.WriteLine("No AVX and/or FMA present, skipping test!");
return;
}
int dataSize = 16 * 16;
Span<Rgb24> data = CreateTestData(dataSize).AsSpan();
var yBlocks = new Block8x8F[4];
var cb = default(Block8x8F);
var cr = default(Block8x8F);
RgbToYCbCrConverterVectorized.Convert420(data, ref yBlocks[0], ref yBlocks[1], ref cb, ref cr, 0);
RgbToYCbCrConverterVectorized.Convert420(data.Slice(16 * 8), ref yBlocks[2], ref yBlocks[3], ref cb, ref cr, 1);
Verify420(data, yBlocks, ref cb, ref cr, new ApproximateFloatComparer(1F));
}
#if SUPPORTS_RUNTIME_INTRINSICS
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public void Scale16x2_8x1(int seed)
{
if (!Avx2.IsSupported)
{
return;
}
Span<float> data = new Random(seed).GenerateRandomFloatArray(Vector256<float>.Count * 4, -1000, 1000);
// Act:
Vector256<float> resultVector = RgbToYCbCrConverterVectorized.Scale16x2_8x1(MemoryMarshal.Cast<float, Vector256<float>>(data));
ref float result = ref Unsafe.As<Vector256<float>, float>(ref resultVector);
// Assert:
// Comparison epsilon is tricky but 10^(-4) is good enough (?)
var comparer = new ApproximateFloatComparer(0.0001f);
for (int i = 0; i < Vector256<float>.Count; i++)
{
float actual = Unsafe.Add(ref result, i);
float expected = CalculateAverage16x2_8x1(data, i);
Assert.True(comparer.Equals(actual, expected), $"Pos {i}, Expected: {expected}, Actual: {actual}");
}
static float CalculateAverage16x2_8x1(Span<float> data, int index)
{
int upIdx = index * 2;
int lowIdx = (index + 8) * 2;
return 0.25f * (data[upIdx] + data[upIdx + 1] + data[lowIdx] + data[lowIdx + 1]);
}
}
#endif
private static void Verify444(
ReadOnlySpan<Rgb24> data,
ref Block8x8F yResult,
ref Block8x8F cbResult,
ref Block8x8F crResult,
ApproximateColorSpaceComparer comparer)
{
Block8x8F y = default;
Block8x8F cb = default;
Block8x8F cr = default;
RgbToYCbCr(data, ref y, ref cb, ref cr);
for (int i = 0; i < Block8x8F.Size; i++)
{
Assert.True(comparer.Equals(new YCbCr(y[i], cb[i], cr[i]), new YCbCr(yResult[i], cbResult[i], crResult[i])), $"Pos {i}, Expected {y[i]} == {yResult[i]}, {cb[i]} == {cbResult[i]}, {cr[i]} == {crResult[i]}");
}
}
private static void Verify420(
ReadOnlySpan<Rgb24> data,
Block8x8F[] yResult,
ref Block8x8F cbResult,
ref Block8x8F crResult,
ApproximateFloatComparer comparer)
{
var trueBlock = default(Block8x8F);
var cbTrue = new Block8x8F[4];
var crTrue = new Block8x8F[4];
Span<Rgb24> tempData = new Rgb24[8 * 8].AsSpan();
// top left
Copy8x8(data, tempData);
RgbToYCbCr(tempData, ref trueBlock, ref cbTrue[0], ref crTrue[0]);
VerifyBlock(ref yResult[0], ref trueBlock, comparer);
// top right
Copy8x8(data.Slice(8), tempData);
RgbToYCbCr(tempData, ref trueBlock, ref cbTrue[1], ref crTrue[1]);
VerifyBlock(ref yResult[1], ref trueBlock, comparer);
// bottom left
Copy8x8(data.Slice(8 * 16), tempData);
RgbToYCbCr(tempData, ref trueBlock, ref cbTrue[2], ref crTrue[2]);
VerifyBlock(ref yResult[2], ref trueBlock, comparer);
// bottom right
Copy8x8(data.Slice((8 * 16) + 8), tempData);
RgbToYCbCr(tempData, ref trueBlock, ref cbTrue[3], ref crTrue[3]);
VerifyBlock(ref yResult[3], ref trueBlock, comparer);
// verify Cb
Scale16X16To8X8(ref trueBlock, cbTrue);
VerifyBlock(ref cbResult, ref trueBlock, comparer);
// verify Cr
Scale16X16To8X8(ref trueBlock, crTrue);
VerifyBlock(ref crResult, ref trueBlock, comparer);
// extracts 8x8 blocks from 16x8 memory region
static void Copy8x8(ReadOnlySpan<Rgb24> source, Span<Rgb24> dest)
{
for (int i = 0; i < 8; i++)
{
source.Slice(i * 16, 8).CopyTo(dest.Slice(i * 8));
}
}
// scales 16x16 to 8x8, used in chroma subsampling tests
static void Scale16X16To8X8(ref Block8x8F dest, ReadOnlySpan<Block8x8F> source)
{
for (int i = 0; i < 4; i++)
{
int dstOff = ((i & 2) << 4) | ((i & 1) << 2);
Block8x8F iSource = source[i];
for (int y = 0; y < 4; y++)
{
for (int x = 0; x < 4; x++)
{
int j = (16 * y) + (2 * x);
float sum = iSource[j] + iSource[j + 1] + iSource[j + 8] + iSource[j + 9];
dest[(8 * y) + x + dstOff] = (sum + 2) * .25F;
}
}
}
}
}
private static void RgbToYCbCr(ReadOnlySpan<Rgb24> data, ref Block8x8F y, ref Block8x8F cb, ref Block8x8F cr)
{
for (int i = 0; i < data.Length; i++)
{
@ -65,17 +240,23 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
int g = data[i].G;
int b = data[i].B;
float y = (0.299F * r) + (0.587F * g) + (0.114F * b);
float cb = 128F + ((-0.168736F * r) - (0.331264F * g) + (0.5F * b));
float cr = 128F + ((0.5F * r) - (0.418688F * g) - (0.081312F * b));
y[i] = (0.299F * r) + (0.587F * g) + (0.114F * b);
cb[i] = 128F + ((-0.168736F * r) - (0.331264F * g) + (0.5F * b));
cr[i] = 128F + ((0.5F * r) - (0.418688F * g) - (0.081312F * b));
}
}
Assert.True(comparer.Equals(new YCbCr(y, cb, cr), new YCbCr(yResult[i], cbResult[i], crResult[i])), $"Pos {i}, Expected {y} == {yResult[i]}, {cb} == {cbResult[i]}, {cr} == {crResult[i]}");
private static void VerifyBlock(ref Block8x8F res, ref Block8x8F target, ApproximateFloatComparer comparer)
{
for (int i = 0; i < Block8x8F.Size; i++)
{
Assert.True(comparer.Equals(res[i], target[i]), $"Pos {i}, Expected: {target[i]}, Got: {res[i]}");
}
}
private static Rgb24[] CreateTestData()
private static Rgb24[] CreateTestData(int size)
{
var data = new Rgb24[64];
var data = new Rgb24[size];
var r = new Random();
var random = new byte[3];

6
tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColorTests.cs

@ -2,7 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
using SixLabors.ImageSharp.PixelFormats;
@ -154,11 +154,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation
[MemberData(nameof(BilevelData))]
[MemberData(nameof(Grayscale4_Data))]
[MemberData(nameof(Grayscale8_Data))]
public void Decode_WritesPixelData(byte[] inputData, int bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult)
public void Decode_WritesPixelData(byte[] inputData, ushort bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult)
{
AssertDecode(expectedResult, pixels =>
{
new BlackIsZeroTiffColor<Rgba32>(new[] { (ushort)bitsPerSample }).Decode(inputData, pixels, left, top, width, height);
new BlackIsZeroTiffColor<Rgba32>(new TiffBitsPerSample(bitsPerSample, 0, 0)).Decode(inputData, pixels, left, top, width, height);
});
}

11
tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/PaletteTiffColorTests.cs

@ -2,7 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
using SixLabors.ImageSharp.PixelFormats;
@ -83,10 +83,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation
[Theory]
[MemberData(nameof(Palette4Data))]
[MemberData(nameof(Palette8Data))]
public void Decode_WritesPixelData(byte[] inputData, ushort bitsPerSample, ushort[] colorMap, int left, int top, int width, int height, Rgba32[][] expectedResult) => AssertDecode(expectedResult, pixels =>
{
new PaletteTiffColor<Rgba32>(new[] { bitsPerSample }, colorMap).Decode(inputData, pixels, left, top, width, height);
});
public void Decode_WritesPixelData(byte[] inputData, ushort bitsPerSample, ushort[] colorMap, int left, int top, int width, int height, Rgba32[][] expectedResult)
=> AssertDecode(expectedResult, pixels =>
{
new PaletteTiffColor<Rgba32>(new TiffBitsPerSample(bitsPerSample, 0, 0), colorMap).Decode(inputData, pixels, left, top, width, height);
});
private static uint[][] GeneratePalette(int count)
{

46
tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColorTests.cs

@ -3,7 +3,7 @@
using System;
using System.Collections.Generic;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -101,17 +101,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation
{
get
{
yield return new object[] { Rgb4Bytes4X4, new ushort[] { 4, 4, 4 }, 0, 0, 4, 4, Rgb4Result4X4 };
yield return new object[] { Rgb4Bytes4X4, new ushort[] { 4, 4, 4 }, 0, 0, 4, 4, Offset(Rgb4Result4X4, 0, 0, 6, 6) };
yield return new object[] { Rgb4Bytes4X4, new ushort[] { 4, 4, 4 }, 1, 0, 4, 4, Offset(Rgb4Result4X4, 1, 0, 6, 6) };
yield return new object[] { Rgb4Bytes4X4, new ushort[] { 4, 4, 4 }, 0, 1, 4, 4, Offset(Rgb4Result4X4, 0, 1, 6, 6) };
yield return new object[] { Rgb4Bytes4X4, new ushort[] { 4, 4, 4 }, 1, 1, 4, 4, Offset(Rgb4Result4X4, 1, 1, 6, 6) };
yield return new object[] { Rgb4Bytes3X4, new ushort[] { 4, 4, 4 }, 0, 0, 3, 4, Rgb4Result3X4 };
yield return new object[] { Rgb4Bytes3X4, new ushort[] { 4, 4, 4 }, 0, 0, 3, 4, Offset(Rgb4Result3X4, 0, 0, 6, 6) };
yield return new object[] { Rgb4Bytes3X4, new ushort[] { 4, 4, 4 }, 1, 0, 3, 4, Offset(Rgb4Result3X4, 1, 0, 6, 6) };
yield return new object[] { Rgb4Bytes3X4, new ushort[] { 4, 4, 4 }, 0, 1, 3, 4, Offset(Rgb4Result3X4, 0, 1, 6, 6) };
yield return new object[] { Rgb4Bytes3X4, new ushort[] { 4, 4, 4 }, 1, 1, 3, 4, Offset(Rgb4Result3X4, 1, 1, 6, 6) };
yield return new object[] { Rgb4Bytes4X4, new TiffBitsPerSample(4, 4, 4), 0, 0, 4, 4, Rgb4Result4X4 };
yield return new object[] { Rgb4Bytes4X4, new TiffBitsPerSample(4, 4, 4), 0, 0, 4, 4, Offset(Rgb4Result4X4, 0, 0, 6, 6) };
yield return new object[] { Rgb4Bytes4X4, new TiffBitsPerSample(4, 4, 4), 1, 0, 4, 4, Offset(Rgb4Result4X4, 1, 0, 6, 6) };
yield return new object[] { Rgb4Bytes4X4, new TiffBitsPerSample(4, 4, 4), 0, 1, 4, 4, Offset(Rgb4Result4X4, 0, 1, 6, 6) };
yield return new object[] { Rgb4Bytes4X4, new TiffBitsPerSample(4, 4, 4), 1, 1, 4, 4, Offset(Rgb4Result4X4, 1, 1, 6, 6) };
yield return new object[] { Rgb4Bytes3X4, new TiffBitsPerSample(4, 4, 4), 0, 0, 3, 4, Rgb4Result3X4 };
yield return new object[] { Rgb4Bytes3X4, new TiffBitsPerSample(4, 4, 4), 0, 0, 3, 4, Offset(Rgb4Result3X4, 0, 0, 6, 6) };
yield return new object[] { Rgb4Bytes3X4, new TiffBitsPerSample(4, 4, 4), 1, 0, 3, 4, Offset(Rgb4Result3X4, 1, 0, 6, 6) };
yield return new object[] { Rgb4Bytes3X4, new TiffBitsPerSample(4, 4, 4), 0, 1, 3, 4, Offset(Rgb4Result3X4, 0, 1, 6, 6) };
yield return new object[] { Rgb4Bytes3X4, new TiffBitsPerSample(4, 4, 4), 1, 1, 3, 4, Offset(Rgb4Result3X4, 1, 1, 6, 6) };
}
}
@ -170,11 +170,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation
{
get
{
yield return new object[] { Rgb8Bytes4X4, new ushort[] { 8, 8, 8 }, 0, 0, 4, 4, Rgb8Result4X4 };
yield return new object[] { Rgb8Bytes4X4, new ushort[] { 8, 8, 8 }, 0, 0, 4, 4, Offset(Rgb8Result4X4, 0, 0, 6, 6) };
yield return new object[] { Rgb8Bytes4X4, new ushort[] { 8, 8, 8 }, 1, 0, 4, 4, Offset(Rgb8Result4X4, 1, 0, 6, 6) };
yield return new object[] { Rgb8Bytes4X4, new ushort[] { 8, 8, 8 }, 0, 1, 4, 4, Offset(Rgb8Result4X4, 0, 1, 6, 6) };
yield return new object[] { Rgb8Bytes4X4, new ushort[] { 8, 8, 8 }, 1, 1, 4, 4, Offset(Rgb8Result4X4, 1, 1, 6, 6) };
yield return new object[] { Rgb8Bytes4X4, new TiffBitsPerSample(8, 8, 8), 0, 0, 4, 4, Rgb8Result4X4 };
yield return new object[] { Rgb8Bytes4X4, new TiffBitsPerSample(8, 8, 8), 0, 0, 4, 4, Offset(Rgb8Result4X4, 0, 0, 6, 6) };
yield return new object[] { Rgb8Bytes4X4, new TiffBitsPerSample(8, 8, 8), 1, 0, 4, 4, Offset(Rgb8Result4X4, 1, 0, 6, 6) };
yield return new object[] { Rgb8Bytes4X4, new TiffBitsPerSample(8, 8, 8), 0, 1, 4, 4, Offset(Rgb8Result4X4, 0, 1, 6, 6) };
yield return new object[] { Rgb8Bytes4X4, new TiffBitsPerSample(8, 8, 8), 1, 1, 4, 4, Offset(Rgb8Result4X4, 1, 1, 6, 6) };
}
}
@ -230,11 +230,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation
{
get
{
yield return new object[] { Rgb484Bytes4X4, new ushort[] { 4, 8, 4 }, 0, 0, 4, 4, Rgb484Result4X4 };
yield return new object[] { Rgb484Bytes4X4, new ushort[] { 4, 8, 4 }, 0, 0, 4, 4, Offset(Rgb484Result4X4, 0, 0, 6, 6) };
yield return new object[] { Rgb484Bytes4X4, new ushort[] { 4, 8, 4 }, 1, 0, 4, 4, Offset(Rgb484Result4X4, 1, 0, 6, 6) };
yield return new object[] { Rgb484Bytes4X4, new ushort[] { 4, 8, 4 }, 0, 1, 4, 4, Offset(Rgb484Result4X4, 0, 1, 6, 6) };
yield return new object[] { Rgb484Bytes4X4, new ushort[] { 4, 8, 4 }, 1, 1, 4, 4, Offset(Rgb484Result4X4, 1, 1, 6, 6) };
yield return new object[] { Rgb484Bytes4X4, new TiffBitsPerSample(4, 8, 4), 0, 0, 4, 4, Rgb484Result4X4 };
yield return new object[] { Rgb484Bytes4X4, new TiffBitsPerSample(4, 8, 4), 0, 0, 4, 4, Offset(Rgb484Result4X4, 0, 0, 6, 6) };
yield return new object[] { Rgb484Bytes4X4, new TiffBitsPerSample(4, 8, 4), 1, 0, 4, 4, Offset(Rgb484Result4X4, 1, 0, 6, 6) };
yield return new object[] { Rgb484Bytes4X4, new TiffBitsPerSample(4, 8, 4), 0, 1, 4, 4, Offset(Rgb484Result4X4, 0, 1, 6, 6) };
yield return new object[] { Rgb484Bytes4X4, new TiffBitsPerSample(4, 8, 4), 1, 1, 4, 4, Offset(Rgb484Result4X4, 1, 1, 6, 6) };
}
}
@ -242,7 +242,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation
[MemberData(nameof(Rgb4Data))]
[MemberData(nameof(Rgb8Data))]
[MemberData(nameof(Rgb484_Data))]
public void Decode_WritesPixelData(byte[][] inputData, ushort[] bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult)
public void Decode_WritesPixelData(byte[][] inputData, TiffBitsPerSample bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult)
{
AssertDecode(expectedResult, pixels =>
{

48
tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/RgbTiffColorTests.cs

@ -2,7 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
using SixLabors.ImageSharp.PixelFormats;
@ -63,17 +63,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation
{
get
{
yield return new object[] { Rgb4Bytes4X4, new ushort[] { 4, 4, 4 }, 0, 0, 4, 4, Rgb4Result4X4 };
yield return new object[] { Rgb4Bytes4X4, new ushort[] { 4, 4, 4 }, 0, 0, 4, 4, Offset(Rgb4Result4X4, 0, 0, 6, 6) };
yield return new object[] { Rgb4Bytes4X4, new ushort[] { 4, 4, 4 }, 1, 0, 4, 4, Offset(Rgb4Result4X4, 1, 0, 6, 6) };
yield return new object[] { Rgb4Bytes4X4, new ushort[] { 4, 4, 4 }, 0, 1, 4, 4, Offset(Rgb4Result4X4, 0, 1, 6, 6) };
yield return new object[] { Rgb4Bytes4X4, new ushort[] { 4, 4, 4 }, 1, 1, 4, 4, Offset(Rgb4Result4X4, 1, 1, 6, 6) };
yield return new object[] { Rgb4Bytes3X4, new ushort[] { 4, 4, 4 }, 0, 0, 3, 4, Rgb4Result3X4 };
yield return new object[] { Rgb4Bytes3X4, new ushort[] { 4, 4, 4 }, 0, 0, 3, 4, Offset(Rgb4Result3X4, 0, 0, 6, 6) };
yield return new object[] { Rgb4Bytes3X4, new ushort[] { 4, 4, 4 }, 1, 0, 3, 4, Offset(Rgb4Result3X4, 1, 0, 6, 6) };
yield return new object[] { Rgb4Bytes3X4, new ushort[] { 4, 4, 4 }, 0, 1, 3, 4, Offset(Rgb4Result3X4, 0, 1, 6, 6) };
yield return new object[] { Rgb4Bytes3X4, new ushort[] { 4, 4, 4 }, 1, 1, 3, 4, Offset(Rgb4Result3X4, 1, 1, 6, 6) };
yield return new object[] { Rgb4Bytes4X4, new TiffBitsPerSample(4, 4, 4), 0, 0, 4, 4, Rgb4Result4X4 };
yield return new object[] { Rgb4Bytes4X4, new TiffBitsPerSample(4, 4, 4), 0, 0, 4, 4, Offset(Rgb4Result4X4, 0, 0, 6, 6) };
yield return new object[] { Rgb4Bytes4X4, new TiffBitsPerSample(4, 4, 4), 1, 0, 4, 4, Offset(Rgb4Result4X4, 1, 0, 6, 6) };
yield return new object[] { Rgb4Bytes4X4, new TiffBitsPerSample(4, 4, 4), 0, 1, 4, 4, Offset(Rgb4Result4X4, 0, 1, 6, 6) };
yield return new object[] { Rgb4Bytes4X4, new TiffBitsPerSample(4, 4, 4), 1, 1, 4, 4, Offset(Rgb4Result4X4, 1, 1, 6, 6) };
yield return new object[] { Rgb4Bytes3X4, new TiffBitsPerSample(4, 4, 4), 0, 0, 3, 4, Rgb4Result3X4 };
yield return new object[] { Rgb4Bytes3X4, new TiffBitsPerSample(4, 4, 4), 0, 0, 3, 4, Offset(Rgb4Result3X4, 0, 0, 6, 6) };
yield return new object[] { Rgb4Bytes3X4, new TiffBitsPerSample(4, 4, 4), 1, 0, 3, 4, Offset(Rgb4Result3X4, 1, 0, 6, 6) };
yield return new object[] { Rgb4Bytes3X4, new TiffBitsPerSample(4, 4, 4), 0, 1, 3, 4, Offset(Rgb4Result3X4, 0, 1, 6, 6) };
yield return new object[] { Rgb4Bytes3X4, new TiffBitsPerSample(4, 4, 4), 1, 1, 3, 4, Offset(Rgb4Result3X4, 1, 1, 6, 6) };
}
}
@ -111,11 +111,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation
{
get
{
yield return new object[] { Rgb8Bytes4X4, new ushort[] { 8, 8, 8 }, 0, 0, 4, 4, Rgb8Result4X4 };
yield return new object[] { Rgb8Bytes4X4, new ushort[] { 8, 8, 8 }, 0, 0, 4, 4, Offset(Rgb8Result4X4, 0, 0, 6, 6) };
yield return new object[] { Rgb8Bytes4X4, new ushort[] { 8, 8, 8 }, 1, 0, 4, 4, Offset(Rgb8Result4X4, 1, 0, 6, 6) };
yield return new object[] { Rgb8Bytes4X4, new ushort[] { 8, 8, 8 }, 0, 1, 4, 4, Offset(Rgb8Result4X4, 0, 1, 6, 6) };
yield return new object[] { Rgb8Bytes4X4, new ushort[] { 8, 8, 8 }, 1, 1, 4, 4, Offset(Rgb8Result4X4, 1, 1, 6, 6) };
yield return new object[] { Rgb8Bytes4X4, new TiffBitsPerSample(8, 8, 8), 0, 0, 4, 4, Rgb8Result4X4 };
yield return new object[] { Rgb8Bytes4X4, new TiffBitsPerSample(8, 8, 8), 0, 0, 4, 4, Offset(Rgb8Result4X4, 0, 0, 6, 6) };
yield return new object[] { Rgb8Bytes4X4, new TiffBitsPerSample(8, 8, 8), 1, 0, 4, 4, Offset(Rgb8Result4X4, 1, 0, 6, 6) };
yield return new object[] { Rgb8Bytes4X4, new TiffBitsPerSample(8, 8, 8), 0, 1, 4, 4, Offset(Rgb8Result4X4, 0, 1, 6, 6) };
yield return new object[] { Rgb8Bytes4X4, new TiffBitsPerSample(8, 8, 8), 1, 1, 4, 4, Offset(Rgb8Result4X4, 1, 1, 6, 6) };
}
}
@ -153,11 +153,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation
{
get
{
yield return new object[] { Rgb484Bytes4X4, new ushort[] { 4, 8, 4 }, 0, 0, 4, 4, Rgb484Result4X4 };
yield return new object[] { Rgb484Bytes4X4, new ushort[] { 4, 8, 4 }, 0, 0, 4, 4, Offset(Rgb484Result4X4, 0, 0, 6, 6) };
yield return new object[] { Rgb484Bytes4X4, new ushort[] { 4, 8, 4 }, 1, 0, 4, 4, Offset(Rgb484Result4X4, 1, 0, 6, 6) };
yield return new object[] { Rgb484Bytes4X4, new ushort[] { 4, 8, 4 }, 0, 1, 4, 4, Offset(Rgb484Result4X4, 0, 1, 6, 6) };
yield return new object[] { Rgb484Bytes4X4, new ushort[] { 4, 8, 4 }, 1, 1, 4, 4, Offset(Rgb484Result4X4, 1, 1, 6, 6) };
yield return new object[] { Rgb484Bytes4X4, new TiffBitsPerSample(4, 8, 4), 0, 0, 4, 4, Rgb484Result4X4 };
yield return new object[] { Rgb484Bytes4X4, new TiffBitsPerSample(4, 8, 4), 0, 0, 4, 4, Offset(Rgb484Result4X4, 0, 0, 6, 6) };
yield return new object[] { Rgb484Bytes4X4, new TiffBitsPerSample(4, 8, 4), 1, 0, 4, 4, Offset(Rgb484Result4X4, 1, 0, 6, 6) };
yield return new object[] { Rgb484Bytes4X4, new TiffBitsPerSample(4, 8, 4), 0, 1, 4, 4, Offset(Rgb484Result4X4, 0, 1, 6, 6) };
yield return new object[] { Rgb484Bytes4X4, new TiffBitsPerSample(4, 8, 4), 1, 1, 4, 4, Offset(Rgb484Result4X4, 1, 1, 6, 6) };
}
}
@ -165,7 +165,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation
[MemberData(nameof(Rgb4Data))]
[MemberData(nameof(Rgb8Data))]
[MemberData(nameof(Rgb484Data))]
public void Decode_WritesPixelData(byte[] inputData, ushort[] bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult)
public void Decode_WritesPixelData(byte[] inputData, TiffBitsPerSample bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult)
{
AssertDecode(expectedResult, pixels =>
{
@ -175,7 +175,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation
[Theory]
[MemberData(nameof(Rgb8Data))]
public void Decode_WritesPixelData_8Bit(byte[] inputData, ushort[] bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult)
public void Decode_WritesPixelData_8Bit(byte[] inputData, TiffBitsPerSample bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult)
{
AssertDecode(expectedResult, pixels =>
{

6
tests/ImageSharp.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColorTests.cs

@ -2,7 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
using SixLabors.ImageSharp.PixelFormats;
@ -154,11 +154,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.PhotometricInterpretation
[MemberData(nameof(BilevelData))]
[MemberData(nameof(Grayscale4Data))]
[MemberData(nameof(Grayscale8Data))]
public void Decode_WritesPixelData(byte[] inputData, int bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult)
public void Decode_WritesPixelData(byte[] inputData, ushort bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult)
{
AssertDecode(expectedResult, pixels =>
{
new WhiteIsZeroTiffColor<Rgba32>(new[] { (ushort)bitsPerSample }).Decode(inputData, pixels, left, top, width, height);
new WhiteIsZeroTiffColor<Rgba32>(new TiffBitsPerSample(bitsPerSample, 0, 0)).Decode(inputData, pixels, left, top, width, height);
});
}

43
tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs

@ -97,30 +97,73 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
public void TiffDecoder_CanDecode_4Bit_WithPalette<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider, ReferenceDecoder, useExactComparer: false, 0.01f);
[Theory]
[WithFile(Flower2BitPalette, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_2Bit_WithPalette<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider, ReferenceDecoder, useExactComparer: false, 0.01f);
[Theory]
[WithFile(Flower2BitGray, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_2Bit<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(FlowerRgb222Contiguous, PixelTypes.Rgba32)]
[WithFile(FlowerRgb222Planar, PixelTypes.Rgba32)]
[WithFile(Flower6BitGray, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_6Bit<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(Flower8BitGray, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_8Bit<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(Flower10BitGray, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_10Bit<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(FlowerRgb444Contiguous, PixelTypes.Rgba32)]
[WithFile(FlowerRgb444Planar, PixelTypes.Rgba32)]
[WithFile(Flower12BitGray, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_12Bit<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(Flower14BitGray, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_14Bit<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(Flower16BitGray, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_16Bit<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(FlowerRgb101010Contiguous, PixelTypes.Rgba32)]
[WithFile(FlowerRgb101010Planar, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_30Bit<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(FlowerRgb121212Contiguous, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_36Bit<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(FlowerRgb141414Contiguous, PixelTypes.Rgba32)]
[WithFile(FlowerRgb141414Planar, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_42Bit<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(FlowerRgb161616Contiguous, PixelTypes.Rgba32)]
[WithFile(FlowerRgb161616Planar, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_48Bit<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(GrayscaleDeflateMultistrip, PixelTypes.Rgba32)]
[WithFile(RgbDeflateMultistrip, PixelTypes.Rgba32)]

41
tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs

@ -78,9 +78,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
}
[Theory]
[InlineData(TiffBitsPerPixel.Bit48)]
[InlineData(TiffBitsPerPixel.Bit42)]
[InlineData(TiffBitsPerPixel.Bit36)]
[InlineData(TiffBitsPerPixel.Bit30)]
[InlineData(TiffBitsPerPixel.Bit12)]
[InlineData(TiffBitsPerPixel.Bit10)]
[InlineData(TiffBitsPerPixel.Bit6)]
public void EncoderOptions_UnsupportedBitPerPixel_DefaultTo24Bits(TiffBitsPerPixel bitsPerPixel)
{
@ -413,7 +416,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
[Theory]
[WithFile(GrayscaleUncompressed, PixelTypes.L8, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.PackBits)]
[WithFile(PaletteDeflateMultistrip, PixelTypes.L8, TiffPhotometricInterpretation.PaletteColor, TiffCompression.Lzw)]
[WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffPhotometricInterpretation.Rgb, TiffCompression.Deflate)]
[WithFile(RgbUncompressed, PixelTypes.Rgb24, TiffPhotometricInterpretation.Rgb, TiffCompression.None)]
[WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffPhotometricInterpretation.Rgb, TiffCompression.None)]
@ -422,6 +424,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
where TPixel : unmanaged, IPixel<TPixel> =>
TestStripLength(provider, photometricInterpretation, compression);
[Theory]
[WithFile(PaletteDeflateMultistrip, PixelTypes.L8, TiffPhotometricInterpretation.PaletteColor, TiffCompression.Lzw)]
public void TiffEncoder_StripLength_WithPalette<TPixel>(TestImageProvider<TPixel> provider, TiffPhotometricInterpretation photometricInterpretation, TiffCompression compression)
where TPixel : unmanaged, IPixel<TPixel> =>
TestStripLength(provider, photometricInterpretation, compression, false, 0.01f);
[Theory]
[WithFile(Calliphora_BiColorUncompressed, PixelTypes.L8, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup3Fax)]
public void TiffEncoder_StripLength_OutOfBounds<TPixel>(TestImageProvider<TPixel> provider, TiffPhotometricInterpretation photometricInterpretation, TiffCompression compression)
@ -429,7 +437,26 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
//// CcittGroup3Fax compressed data length can be larger than the original length.
Assert.Throws<Xunit.Sdk.TrueException>(() => TestStripLength(provider, photometricInterpretation, compression));
private static void TestStripLength<TPixel>(TestImageProvider<TPixel> provider, TiffPhotometricInterpretation photometricInterpretation, TiffCompression compression)
[Theory]
[WithTestPatternImages(287, 321, PixelTypes.Rgba32, TiffPhotometricInterpretation.Rgb)]
[WithTestPatternImages(287, 321, PixelTypes.Rgba32, TiffPhotometricInterpretation.PaletteColor)]
[WithTestPatternImages(287, 321, PixelTypes.Rgba32, TiffPhotometricInterpretation.BlackIsZero)]
public void TiffEncode_WorksWithDiscontiguousBuffers<TPixel>(TestImageProvider<TPixel> provider, TiffPhotometricInterpretation photometricInterpretation)
where TPixel : unmanaged, IPixel<TPixel>
{
provider.LimitAllocatorBufferCapacity().InPixelsSqrt(200);
using Image<TPixel> image = provider.GetImage();
var encoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation };
image.DebugSave(provider, encoder);
}
private static void TestStripLength<TPixel>(
TestImageProvider<TPixel> provider,
TiffPhotometricInterpretation photometricInterpretation,
TiffCompression compression,
bool useExactComparer = true,
float compareTolerance = 0.01f)
where TPixel : unmanaged, IPixel<TPixel>
{
// arrange
@ -477,10 +504,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
// Compare with reference.
TestTiffEncoderCore(
provider,
inputMeta.BitsPerPixel,
photometricInterpretation,
inputCompression);
provider,
inputMeta.BitsPerPixel,
photometricInterpretation,
inputCompression,
useExactComparer: useExactComparer,
compareTolerance: compareTolerance);
}
private static void TestTiffEncoderCore<TPixel>(

2
tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs

@ -66,7 +66,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
{
Assert.NotNull(frameMetaData);
Assert.NotNull(frameMetaData.BitsPerPixel);
Assert.Equal(TiffBitsPerSample.Bit4, (TiffBitsPerSample)frameMetaData.BitsPerPixel);
Assert.Equal(TiffBitsPerPixel.Bit4, frameMetaData.BitsPerPixel);
Assert.Equal(TiffCompression.Lzw, frameMetaData.Compression);
Assert.Equal(TiffPhotometricInterpretation.PaletteColor, frameMetaData.PhotometricInterpretation);
Assert.Equal(TiffPredictor.None, frameMetaData.Predictor);

2
tests/ImageSharp.Tests/ImageSharp.Tests.csproj

@ -6,7 +6,7 @@
<AssemblyName>SixLabors.ImageSharp.Tests</AssemblyName>
<Platforms>AnyCPU;x64;x86</Platforms>
<RootNamespace>SixLabors.ImageSharp.Tests</RootNamespace>
<Configurations>Debug;Release;Release-InnerLoop;Debug-InnerLoop</Configurations>
<Configurations>Debug;Release;Debug-InnerLoop;Release-InnerLoop</Configurations>
</PropertyGroup>
<Choose>

13
tests/ImageSharp.Tests/Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs

@ -223,19 +223,6 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
Assert.Equal(0, buffer.Memory.Length);
}
[Theory]
[InlineData(101)]
[InlineData((int.MaxValue / SizeOfLargeStruct) - 1)]
[InlineData(int.MaxValue / SizeOfLargeStruct)]
[InlineData((int.MaxValue / SizeOfLargeStruct) + 1)]
[InlineData((int.MaxValue / SizeOfLargeStruct) + 137)]
public void Allocate_OverCapacity_Throws_InvalidMemoryOperationException(int length)
{
this.LocalFixture.MemoryAllocator.BufferCapacityInBytes = 100 * SizeOfLargeStruct;
Assert.Throws<InvalidMemoryOperationException>(() =>
this.LocalFixture.MemoryAllocator.Allocate<LargeStruct>(length));
}
[Theory]
[InlineData(-1)]
public void AllocateManagedByteBuffer_IncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length)

9
tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs

@ -11,6 +11,7 @@ using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Convolution;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using Xunit;
using Xunit.Abstractions;
@ -154,8 +155,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution
appendSourceFileOrDescription: false);
[Theory]
[WithFileCollection(nameof(TestFiles), nameof(BokehBlurValues), PixelTypes.Rgba32)]
public void BokehBlurFilterProcessor_Bounded(TestImageProvider<Rgba32> provider, BokehBlurInfo value)
[WithFileCollection(nameof(TestFiles), nameof(BokehBlurValues), PixelTypes.Rgba32, HwIntrinsics.AllowAll)]
[WithFileCollection(nameof(TestFiles), nameof(BokehBlurValues), PixelTypes.Rgba32, HwIntrinsics.DisableSSE41)]
public void BokehBlurFilterProcessor_Bounded(TestImageProvider<Rgba32> provider, BokehBlurInfo value, HwIntrinsics intrinsicsFilter)
{
static void RunTest(string arg1, string arg2)
{
@ -173,12 +175,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution
x.BokehBlur(value.Radius, value.Components, value.Gamma, bounds);
},
testOutputDetails: value.ToString(),
ImageComparer.TolerantPercentage(0.05f),
appendPixelTypeToFileName: false);
}
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
HwIntrinsics.DisableSSE41,
intrinsicsFilter,
provider,
value);
}

4
tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Dithering;
@ -172,8 +173,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Dithering
provider.RunBufferCapacityLimitProcessorTest(
41,
c => c.Dither(dither),
name,
ImageComparer.TolerantPercentage(0.001f));
name);
}
}
}

3
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs

@ -80,6 +80,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{ KnownResamplers.Bicubic, 1680, 1200 },
{ KnownResamplers.Box, 13, 299 },
{ KnownResamplers.Lanczos5, 3032, 600 },
// Large number. https://github.com/SixLabors/ImageSharp/issues/1616
{ KnownResamplers.Bicubic, 207773, 51943 }
};
public static TheoryData<string, int, int> GeneratedImageResizeData =

15
tests/ImageSharp.Tests/TestImages.cs

@ -735,16 +735,27 @@ namespace SixLabors.ImageSharp.Tests
public const string RgbPalette = "Tiff/rgb_palette.tiff";
public const string Rgb4BitPalette = "Tiff/bike_colorpalette_4bit.tiff";
public const string RgbPaletteDeflate = "Tiff/rgb_palette_deflate.tiff";
public const string Flower4BitPalette = "Tiff/flower-palette-04.tiff";
public const string Flower4BitPaletteGray = "Tiff/flower-minisblack-04.tiff";
public const string FlowerRgb161616Contiguous = "Tiff/flower-rgb-contig-16.tiff";
public const string FlowerRgb161616Planar = "Tiff/flower-rgb-planar-16.tiff";
public const string FlowerRgb141414Contiguous = "Tiff/flower-rgb-contig-14.tiff";
public const string FlowerRgb141414Planar = "Tiff/flower-rgb-planar-14.tiff";
public const string FlowerRgb101010Contiguous = "Tiff/flower-rgb-contig-10.tiff";
public const string FlowerRgb101010Planar = "Tiff/flower-rgb-planar-10.tiff";
public const string FlowerRgb121212Contiguous = "Tiff/flower-rgb-contig-12.tiff";
public const string FlowerRgb444Contiguous = "Tiff/flower-rgb-contig-04.tiff";
public const string FlowerRgb444Planar = "Tiff/flower-rgb-planar-04.tiff";
public const string FlowerRgb222Contiguous = "Tiff/flower-rgb-contig-02.tiff";
public const string FlowerRgb222Planar = "Tiff/flower-rgb-planar-02.tiff";
public const string Flower2BitGray = "Tiff/flower-minisblack-02.tiff";
public const string Flower2BitPalette = "Tiff/flower-palette-02.tiff";
public const string Flower4BitPalette = "Tiff/flower-palette-04.tiff";
public const string Flower4BitPaletteGray = "Tiff/flower-minisblack-04.tiff";
public const string Flower6BitGray = "Tiff/flower-minisblack-06.tiff";
public const string Flower8BitGray = "Tiff/flower-minisblack-08.tiff";
public const string Flower10BitGray = "Tiff/flower-minisblack-10.tiff";
public const string Flower12BitGray = "Tiff/flower-minisblack-12.tiff";
public const string Flower14BitGray = "Tiff/flower-minisblack-14.tiff";
public const string Flower16BitGray = "Tiff/flower-minisblack-16.tiff";
public const string SmallRgbDeflate = "Tiff/rgb_small_deflate.tiff";
public const string SmallRgbLzw = "Tiff/rgb_small_lzw.tiff";

2
tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs

@ -84,7 +84,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs
MemoryGroup<TPixel> framePixels = frame.PixelBuffer.FastMemoryGroup;
using IUnsafePixelCollection<ushort> pixels = magicFrame.GetPixelsUnsafe();
if (magicFrame.Depth == 8 || magicFrame.Depth == 4 || magicFrame.Depth == 2 || magicFrame.Depth == 1 || magicFrame.Depth == 10)
if (magicFrame.Depth == 8 || magicFrame.Depth == 6 || magicFrame.Depth == 4 || magicFrame.Depth == 2 || magicFrame.Depth == 1 || magicFrame.Depth == 10 || magicFrame.Depth == 12)
{
byte[] data = pixels.ToByteArray(PixelMapping.RGBA);

17
tests/ImageSharp.Tests/TestUtilities/Tests/TestEnvironmentTests.cs

@ -3,6 +3,8 @@
using System;
using System.IO;
using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Gif;
@ -117,5 +119,20 @@ namespace SixLabors.ImageSharp.Tests
IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(fileName);
Assert.IsType(expectedDecoderType, decoder);
}
// RemoteExecutor does not work with "dotnet xunit" used to run tests on 32 bit .NET Framework:
// https://github.com/SixLabors/ImageSharp/blob/381dff8640b721a34b1227c970fcf6ad6c5e3e72/ci-test.ps1#L30
public static bool IsNot32BitNetFramework = !TestEnvironment.IsFramework || TestEnvironment.Is64BitProcess;
[ConditionalFact(nameof(IsNot32BitNetFramework))]
public void RemoteExecutor_FailingRemoteTestShouldFailLocalTest()
{
static void FailingCode()
{
Assert.False(true);
}
Assert.ThrowsAny<RemoteExecutionException>(() => RemoteExecutor.Invoke(FailingCode).Dispose());
}
}
}

2
tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithOctreeQuantizer_rgb32.bmp

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6dba331639d724f198d7d11af971156d34076b57bba0f2d0d45e699104a3a674
oid sha256:11375b15df083d98335f4a4baf0717e7fdd6b21ab2132a6815cadc787ac17e7d
size 9270

2
tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithWuQuantizer_rgb32.bmp

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9213e188b3a2f715ae21a5ab2fb2acedc23397207820c0999b06fa60e7052b85
oid sha256:e063e97cd8a000de6830adcc3961a7dc41785d40cd4d83af10ca38d96e071362
size 9270

4
tests/Images/External/ReferenceOutput/DitherTests/ApplyDiffusionFilterInBox_Rgba32_CalliphoraPartial.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1aa62e798c085eb7b0e8e5ce5e4cb2cccfe925dd8ac3e29659f9afd53fca977c
size 329912
oid sha256:cafc426ac8e8d02a87f67c90e8c1976c5fae0e12b49deae52ad08476f7ed49a4
size 266391

4
tests/Images/External/ReferenceOutput/DitherTests/ApplyDitherFilterInBox_Rgba32_CalliphoraPartial.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d0c8ccdfbf6b1c961f6531ae61207a7f89507f469c875677f1755ea3d6c8d900
size 326504
oid sha256:4d88eb2e50ca9dbed0e8dfe4ad278cd88ddb9d3408b30d9dfe59102b167f570b
size 262887

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_ShouldNotDependOnSinglePixelType_Bgra32_filter0.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0216f1684430087035b387ab02d33b043c526bd8c7d78343c31e1bc410581bfb
size 727
oid sha256:0369747820c86bb692fc7b75f3519095c9b2a58a885ebd37c871c103d08405a0
size 720

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_ShouldNotDependOnSinglePixelType_Rgb24_filter0.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0216f1684430087035b387ab02d33b043c526bd8c7d78343c31e1bc410581bfb
size 727
oid sha256:0369747820c86bb692fc7b75f3519095c9b2a58a885ebd37c871c103d08405a0
size 720

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_ShouldNotDependOnSinglePixelType_Rgba32_filter0.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0216f1684430087035b387ab02d33b043c526bd8c7d78343c31e1bc410581bfb
size 727
oid sha256:0369747820c86bb692fc7b75f3519095c9b2a58a885ebd37c871c103d08405a0
size 720

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_ShouldNotDependOnSinglePixelType_RgbaVector_filter0.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:24191da3ce18438edefa7a189d9beadaa3057b5e4d4c550254e3a81ed159c0f8
size 723
oid sha256:f63aebed17504ef50d96ac7e58dc41f5227a83a38810359ed8e9cecda137183b
size 720

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Atkinson.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:087148425a048f33c6ae063064cfe374f7fb88f075d767e62c73675ec52a3e0a
size 100066
oid sha256:471eaf2e532b40592c86dc816709d3ae4bbd64892006e00fd611ef6869d3b934
size 52070

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Burks.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b2a90c8463632606b40461ad91d80d44826f7b468ba5f1a905acfc85ad0344c9
size 114413
oid sha256:91fb9966a4b3eaefd5533ddf0b98ec08fbf8cbc263e4ebd438895e6d4129dd03
size 61447

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_FloydSteinberg.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:192c742bfd53f3a74d96c79e92443a922ac60c354b73d7abf292f30d10131307
size 114842
oid sha256:d74faa8d188a2915739de64ba9d71b2132b53c8d154db22510c524ae757578a5
size 61183

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_JarvisJudiceNinke.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:50c12659dc05b3ce8a6692cdbc72971bbc691cc7fd26c34df65b4bd71d190e5b
size 108799
oid sha256:080cc89d1d6568a2c9b707bf05428ab5febd2951e37223f96e349cc6646d32aa
size 56070

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Sierra2.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6a1c408687899b57b96e9f01ea889bc6f16d9a479386346c6bd9babc45ef99d0
size 109095
oid sha256:c7589986c1a762d52fe8ffc252e9938ff0e3a9e00b91ea7f5e36d4335b2b7870
size 58502

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Sierra3.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d3d8aead7f9f69dac7eec1b2d8e2200331bf28218c98b7fa3435c9610ff88264
size 110221
oid sha256:934042746c3a9b652069da26b479e2be7cbdb17ab20e41c5e271013a76e96e46
size 58480

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_SierraLite.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f1fed9d8f58e38cfa938d7735cbdfcbac0aa02f58eda0dddfe29a5ebed0e74eb
size 117802
oid sha256:03d5d5cbf1b2c0be736aa2bf726ad4bb04fca77aff393edb9663a7915a794264
size 62418

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_StevensonArce.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dbeeda3f4e505c46e1ec218001f0f1aade4eb64c3932e2c374a74c4b0702d7a8
size 103735
oid sha256:19a0d8667bfd01e18adbfca778e868ea7a6c43d427f9ae40eb4281d438ef509c
size 54464

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_Bike_Stucki.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8003014c90f6c3a722c75e9cafb397d1be3818bb84c3484e28ee79ae273d7d0b
size 109707
oid sha256:11c1056e013292e0543598f5690625b9bac0420a15fd1f37f6484daa3b8326fa
size 60074

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Atkinson.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f02a9465aaaa62b6fc0e9e0578362ccf65ce57bf7a8e1e2899f254863e72807f
size 100060
oid sha256:3fcf9b7e4ee34e80e8811f94940aff09a5392c21019fc86b145d16fd9c6b1cd2
size 57501

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_Burks.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f50c240c062cb66e820e8f632107e63bc0de85013143a81975e56f0b72499d8f
size 102871
oid sha256:4f0d9a43d8a47e00f6e5932b57f99565370a7239496fdbe162fb774497c4ef2a
size 59377

4
tests/Images/External/ReferenceOutput/DitherTests/DiffusionFilter_WorksWithAllErrorDiffusers_CalliphoraPartial_FloydSteinberg.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:aa6c2bb78faaf689cf979dd87b3a24b6405720755ce16c028cafab690ac7b318
size 104334
oid sha256:d4a64da29f144d4d4c525ea45e56819e02a46030ae09542be01fdd8ffc85a295
size 60377

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

Loading…
Cancel
Save