Browse Source

Merge remote-tracking branch 'upstream/main' into heic-support

# Conflicts:
#	src/ImageSharp/Configuration.cs
#	tests/ImageSharp.Tests/ConfigurationTests.cs
pull/2633/head
James Jackson-South 1 week ago
parent
commit
8e424d1a4a
  1. 20
      .gitattributes
  2. 3
      .github/copilot-instructions.md
  3. 89
      .github/workflows/build-and-test.yml
  4. 19
      .github/workflows/code-coverage.yml
  5. 2
      .gitignore
  6. 41
      AGENTS.md
  7. 3
      CLAUDE.md
  8. 4
      Directory.Build.props
  9. 3
      GEMINI.md
  10. 12
      ImageSharp.sln
  11. 12
      README.md
  12. 96
      SECURITY.md
  13. 18
      global.json
  14. 2
      shared-infrastructure
  15. 9
      src/ImageSharp/Advanced/AdvancedImageExtensions.cs
  16. 179
      src/ImageSharp/Advanced/AotCompilerTools.cs
  17. 21
      src/ImageSharp/Advanced/IImageFrameVisitor.cs
  18. 4
      src/ImageSharp/Advanced/IImageVisitor.cs
  19. 4
      src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs
  20. 19
      src/ImageSharp/Advanced/ParallelExecutionSettings.cs
  21. 59
      src/ImageSharp/Advanced/ParallelRowIterator.cs
  22. 6
      src/ImageSharp/Color/Color.WernerPalette.cs
  23. 199
      src/ImageSharp/Color/Color.cs
  24. 32
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs
  25. 32
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs
  26. 32
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs
  27. 32
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs
  28. 32
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs
  29. 32
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs
  30. 124
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
  31. 192
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs
  32. 32
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs
  33. 32
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs
  34. 32
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs
  35. 2
      src/ImageSharp/ColorProfiles/Icc/Calculators/GrayTrcCalculator.cs
  36. 17
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs
  37. 141
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs
  38. 2
      src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs
  39. 8
      src/ImageSharp/ColorProfiles/KnownIlluminants.cs
  40. 184
      src/ImageSharp/ColorProfiles/Rgb.cs
  41. 1
      src/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs
  42. 6
      src/ImageSharp/ColorProfiles/WorkingSpaces/RgbWorkingSpace.cs
  43. 4
      src/ImageSharp/Common/Extensions/StreamExtensions.cs
  44. 67
      src/ImageSharp/Common/Helpers/ColorNumerics.cs
  45. 409
      src/ImageSharp/Common/Helpers/Numerics.cs
  46. 32
      src/ImageSharp/Common/Helpers/Shuffle/IComponentShuffle.cs
  47. 136
      src/ImageSharp/Common/Helpers/Shuffle/IPad3Shuffle4.cs
  48. 45
      src/ImageSharp/Common/Helpers/Shuffle/IShuffle3.cs
  49. 354
      src/ImageSharp/Common/Helpers/Shuffle/IShuffle4.cs
  50. 137
      src/ImageSharp/Common/Helpers/Shuffle/IShuffle4Slice3.cs
  51. 31
      src/ImageSharp/Common/Helpers/SimdUtils.Convert.cs
  52. 232
      src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
  53. 378
      src/ImageSharp/Common/Helpers/SimdUtils.Shuffle.cs
  54. 6
      src/ImageSharp/Common/Helpers/SimdUtils.cs
  55. 554
      src/ImageSharp/Common/Helpers/Vector128Utilities.cs
  56. 126
      src/ImageSharp/Common/Helpers/Vector256Utilities.cs
  57. 61
      src/ImageSharp/Common/Helpers/Vector512Utilities.cs
  58. 52
      src/ImageSharp/Common/InlineArray.cs
  59. 2
      src/ImageSharp/Common/InlineArray.tt
  60. 72
      src/ImageSharp/Common/Tuples/Octet{T}.cs
  61. 435
      src/ImageSharp/Compression/Zlib/Adler32.cs
  62. 120
      src/ImageSharp/Compression/Zlib/ChunkedReadStream.cs
  63. 143
      src/ImageSharp/Compression/Zlib/ChunkedWriteStream.cs
  64. 33
      src/ImageSharp/Compression/Zlib/DeflateThrowHelper.cs
  65. 290
      src/ImageSharp/Compression/Zlib/Deflater.cs
  66. 148
      src/ImageSharp/Compression/Zlib/DeflaterConstants.cs
  67. 867
      src/ImageSharp/Compression/Zlib/DeflaterEngine.cs
  68. 979
      src/ImageSharp/Compression/Zlib/DeflaterHuffman.cs
  69. 143
      src/ImageSharp/Compression/Zlib/DeflaterOutputStream.cs
  70. 185
      src/ImageSharp/Compression/Zlib/DeflaterPendingBuffer.cs
  71. 11
      src/ImageSharp/Compression/Zlib/README.md
  72. 177
      src/ImageSharp/Compression/Zlib/ZlibDeflateStream.cs
  73. 120
      src/ImageSharp/Compression/Zlib/ZlibInflateReader.cs
  74. 277
      src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
  75. 19
      src/ImageSharp/Configuration.cs
  76. 73
      src/ImageSharp/Formats/Ani/AniChunkType.cs
  77. 25
      src/ImageSharp/Formats/Ani/AniConfigurationModule.cs
  78. 54
      src/ImageSharp/Formats/Ani/AniConstants.cs
  79. 52
      src/ImageSharp/Formats/Ani/AniDecoder.cs
  80. 862
      src/ImageSharp/Formats/Ani/AniDecoderCore.cs
  81. 24
      src/ImageSharp/Formats/Ani/AniEncoder.cs
  82. 514
      src/ImageSharp/Formats/Ani/AniEncoderCore.cs
  83. 40
      src/ImageSharp/Formats/Ani/AniFormat.cs
  84. 25
      src/ImageSharp/Formats/Ani/AniFrameFormat.cs
  85. 248
      src/ImageSharp/Formats/Ani/AniFrameMetadata.cs
  86. 114
      src/ImageSharp/Formats/Ani/AniFrameStream.cs
  87. 98
      src/ImageSharp/Formats/Ani/AniHeader.cs
  88. 21
      src/ImageSharp/Formats/Ani/AniHeaderFlags.cs
  89. 39
      src/ImageSharp/Formats/Ani/AniImageFormatDetector.cs
  90. 134
      src/ImageSharp/Formats/Ani/AniMetadata.cs
  91. 34
      src/ImageSharp/Formats/Ani/AniRiffChunkHeader.cs
  92. 2
      src/ImageSharp/Formats/Bmp/BmpConstants.cs
  93. 71
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  94. 2
      src/ImageSharp/Formats/Bmp/BmpEncoder.cs
  95. 89
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  96. 6
      src/ImageSharp/Formats/Cur/CurConfigurationModule.cs
  97. 10
      src/ImageSharp/Formats/Cur/CurConstants.cs
  98. 7
      src/ImageSharp/Formats/Cur/CurDecoder.cs
  99. 10
      src/ImageSharp/Formats/Cur/CurDecoderCore.cs
  100. 5
      src/ImageSharp/Formats/Cur/CurEncoder.cs

20
.gitattributes

@ -84,19 +84,16 @@
# treat as binary
###############################################################################
*.basis binary
*.a binary
*.dll binary
*.eot binary
*.dylib binary
*.exe binary
*.otf binary
*.pdf binary
*.ppt binary
*.pptx binary
*.pvr binary
*.so binary
*.snk binary
*.ttc binary
*.ttf binary
*.woff binary
*.woff2 binary
*.xls binary
*.xlsx binary
###############################################################################
@ -113,6 +110,7 @@
###############################################################################
# Handle image files by git lfs
###############################################################################
*.pdf filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
*.bmp filter=lfs diff=lfs merge=lfs -text
@ -126,6 +124,7 @@
*.dds filter=lfs diff=lfs merge=lfs -text
*.ktx filter=lfs diff=lfs merge=lfs -text
*.ktx2 filter=lfs diff=lfs merge=lfs -text
*.astc filter=lfs diff=lfs merge=lfs -text
*.pam filter=lfs diff=lfs merge=lfs -text
*.pbm filter=lfs diff=lfs merge=lfs -text
*.pgm filter=lfs diff=lfs merge=lfs -text
@ -143,3 +142,12 @@
# Handle ICC files by git lfs
###############################################################################
*.icc filter=lfs diff=lfs merge=lfs -text
###############################################################################
# Handle font files by git lfs
###############################################################################
*.eot filter=lfs diff=lfs merge=lfs -text
*.otf filter=lfs diff=lfs merge=lfs -text
*.ttc filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text
*.woff filter=lfs diff=lfs merge=lfs -text
*.woff2 filter=lfs diff=lfs merge=lfs -text

3
.github/copilot-instructions.md

@ -0,0 +1,3 @@
# GitHub Copilot Instructions
Read and follow [AGENTS.md](../AGENTS.md) as the repository-wide source of coding, performance, and verification requirements. Prefer existing local patterns and repository configuration whenever generated code or suggestions are accepted.

89
.github/workflows/build-and-test.yml

@ -11,7 +11,7 @@ on:
branches:
- main
- release/*
types: [ labeled, opened, synchronize, reopened ]
types: [ opened, synchronize, reopened ]
jobs:
# Prime a single LFS cache and expose the exact key for the matrix
@ -27,7 +27,7 @@ jobs:
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: recursive
@ -49,7 +49,7 @@ jobs:
run: echo "lfs_key=$LFS_KEY" >> "$GITHUB_OUTPUT"
- name: Git Setup LFS Cache
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: .git/lfs
key: ${{ steps.expose-key.outputs.lfs_key }}
@ -62,58 +62,52 @@ jobs:
needs: WarmLFS
strategy:
matrix:
isARM:
- ${{ contains(github.event.pull_request.labels.*.name, 'arch:arm32') || contains(github.event.pull_request.labels.*.name, 'arch:arm64') }}
options:
- os: ubuntu-latest
framework: net9.0
sdk: 9.0.x
framework: net11.0
sdk: 11.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable
framework: net9.0
sdk: 9.0.x
- os: macos-26
framework: net11.0
sdk: 11.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: windows-latest
framework: net9.0
sdk: 9.0.x
framework: net11.0
sdk: 11.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: buildjet-4vcpu-ubuntu-2204-arm
framework: net9.0
sdk: 9.0.x
- os: ubuntu-22.04-arm
framework: net11.0
sdk: 11.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: ubuntu-latest
framework: net8.0
sdk: 8.0.x
framework: net10.0
sdk: 10.0.x
runtime: -x64
codecov: false
- os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable
framework: net8.0
sdk: 8.0.x
- os: macos-26
framework: net10.0
sdk: 10.0.x
runtime: -x64
codecov: false
- os: windows-latest
framework: net8.0
sdk: 8.0.x
framework: net10.0
sdk: 10.0.x
runtime: -x64
codecov: false
- os: buildjet-4vcpu-ubuntu-2204-arm
framework: net8.0
sdk: 8.0.x
- os: ubuntu-22.04-arm
framework: net10.0
sdk: 10.0.x
runtime: -x64
codecov: false
exclude:
- isARM: false
options:
os: buildjet-4vcpu-ubuntu-2204-arm
runs-on: ${{ matrix.options.os }}
@ -124,6 +118,18 @@ jobs:
sudo apt-get update
sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev
- name: Install libgdi+, which is required for tests running on macos
if: ${{ contains(matrix.options.os, 'macos-26') }}
run: |
brew update
brew install mono-libgdiplus
# Create symlinks to make libgdiplus discoverable
sudo mkdir -p /usr/local/lib
sudo ln -sf $(brew --prefix)/lib/libgdiplus.dylib /usr/local/lib/libgdiplus.dylib
# Verify installation
ls -la $(brew --prefix)/lib/libgdiplus* || echo "libgdiplus not found in brew prefix"
ls -la /usr/local/lib/libgdiplus* || echo "libgdiplus not found in /usr/local/lib"
- name: Git Config
shell: bash
run: |
@ -131,14 +137,14 @@ jobs:
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: recursive
# Use the warmed key from WarmLFS. Do not recompute or recreate .lfs-assets-id here.
- name: Git Setup LFS Cache
uses: actions/cache@v4
uses: actions/cache@v6
with:
path: .git/lfs
key: ${{ needs.WarmLFS.outputs.lfs_key }}
@ -148,10 +154,10 @@ jobs:
run: git lfs pull
- name: NuGet Install
uses: NuGet/setup-nuget@v2
uses: NuGet/setup-nuget@v4
- name: NuGet Setup Cache
uses: actions/cache@v4
uses: actions/cache@v6
id: nuget-cache
with:
path: ~/.nuget
@ -160,17 +166,18 @@ jobs:
- name: DotNet Setup
if: ${{ matrix.options.sdk-preview != true }}
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v6
with:
dotnet-version: |
8.0.x
10.0.x
- name: DotNet Setup Preview
if: ${{ matrix.options.sdk-preview == true }}
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v6
with:
dotnet-quality: preview
dotnet-version: |
9.0.x
11.0.x
- name: DotNet Build
if: ${{ matrix.options.sdk-preview != true }}
@ -203,7 +210,7 @@ jobs:
XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit
- name: Export Failed Output
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
if: failure()
with:
name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip
@ -221,16 +228,16 @@ jobs:
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: recursive
- name: NuGet Install
uses: NuGet/setup-nuget@v2
uses: NuGet/setup-nuget@v4
- name: NuGet Setup Cache
uses: actions/cache@v4
uses: actions/cache@v6
id: nuget-cache
with:
path: ~/.nuget

19
.github/workflows/code-coverage.yml

@ -11,7 +11,7 @@ jobs:
matrix:
options:
- os: ubuntu-latest
framework: net8.0
framework: net10.0
runtime: -x64
codecov: true
@ -31,7 +31,7 @@ jobs:
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: recursive
@ -46,7 +46,7 @@ jobs:
run: git lfs ls-files -l | awk '{print $1}' | sort > .lfs-assets-id
- name: Git Setup LFS Cache
uses: actions/cache@v4
uses: actions/cache@v6
id: lfs-cache
with:
path: .git/lfs
@ -56,10 +56,10 @@ jobs:
run: git lfs pull
- name: NuGet Install
uses: NuGet/setup-nuget@v2
uses: NuGet/setup-nuget@v4
- name: NuGet Setup Cache
uses: actions/cache@v4
uses: actions/cache@v6
id: nuget-cache
with:
path: ~/.nuget
@ -67,10 +67,10 @@ jobs:
restore-keys: ${{ runner.os }}-nuget-
- name: DotNet Setup
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v6
with:
dotnet-version: |
8.0.x
10.0.x
- name: DotNet Build
shell: pwsh
@ -86,14 +86,15 @@ jobs:
XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit
- name: Export Failed Output
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
if: failure()
with:
name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip
path: tests/Images/ActualOutput/
- name: Codecov Update
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v7
if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors')
with:
flags: unittests
token: ${{ secrets.CODECOV_TOKEN }}

2
.gitignore

@ -227,3 +227,5 @@ artifacts/
#lfs
hooks/**
lfs/**
.dotnet

41
AGENTS.md

@ -0,0 +1,41 @@
# Six Labors AI Coding Guidelines
These instructions apply to the entire repository. More-specific `AGENTS.md` files may add to or override them for their directory tree.
## Working Practices
- Inspect the relevant implementation, tests, benchmarks, project files, and nearby code before proposing or making changes. Do not infer current behavior when the source is available.
- Make the smallest complete change that solves the requested problem. Avoid unrelated cleanup, speculative abstractions, and formatting churn.
- Match established architecture, naming, formatting, documentation, and test patterns. Treat `.editorconfig`, analyzers, and repository build settings as authoritative.
- Preserve public API and observable behavior unless the task explicitly requires a change. Public API documentation must describe observable behavior, not implementation details.
- Do not use reflection against built assemblies, ad hoc assembly loading, or temporary probe projects unless explicitly requested.
- Build .NET projects in Release configuration unless explicitly instructed otherwise.
## Performance
- Treat throughput, latency, memory use, and binary size as design constraints, especially in pixel-processing, drawing, parsing, encoding, and other hot paths.
- Avoid unnecessary allocations, copies, boxing, closures, interface dispatch, repeated enumeration, and extra passes over data.
- Reuse the repository's existing memory ownership, pooling, span, vectorization, and parallelization patterns. Do not introduce a new mechanism when an established one fits.
- Keep hot loops simple and bounds-check-friendly. Hoist invariant work, preserve locality, and use the narrowest suitable data types without sacrificing correctness.
- Do not trade correctness or maintainability for assumed speed. Support non-obvious optimizations with measurements or clear evidence, and add or update benchmarks when performance is the purpose of the change.
- Consider all supported target frameworks and runtime capabilities. Do not regress fallback paths while optimizing newer runtimes.
## C# Conventions
- Follow the existing code around the change; local patterns take precedence over generic preferences.
- Do not use `record` or `record struct` types.
- Prefer established invariants over redundant guards. Validate at real external boundaries and do not add defensive checks for internally controlled states.
- Do not extract single-use helpers merely to name a block. Extract only for genuine reuse, an established local pattern, or meaningful complexity reduction.
- Add vertical whitespace after multi-line statements and declarations and between distinct logical stages. Never add trailing whitespace.
- Document every method, constructor, and property, regardless of whether it is public, internal, protected, or private. Keep public API documentation limited to observable behavior; use private and internal documentation to capture the contract and intent needed to maintain the code.
- Add inline comments throughout complex code. Explain algorithms, formulas, invariants, ownership, compatibility behavior, and performance tradeoffs at the operations and decisions they govern. Explain why the code is shaped that way rather than narrating the syntax.
- Document SIMD code especially thoroughly. Explain the vector layout, lane meaning, widening or narrowing, masks, shuffles, constants, alignment or remainder handling, supported instruction paths, scalar equivalence, and the reason each non-obvious operation is correct.
- Write algorithm and SIMD comments for a maintainer who is unfamiliar with the implementation. The reader should not need to reconstruct intent from external documentation, issue history, or benchmark results.
## Verification
- Add or update focused tests when behavior changes, following the test framework and conventions already used by the project.
- Never hack, weaken, skip, conditionally bypass, or otherwise manipulate a test to make it pass. Fix the production defect or the genuine test defect while preserving the test's intended coverage and sensitivity.
- Do not update golden files, reference images, snapshots, baselines, or expected-output artifacts to resolve a test failure. Treat a mismatch as evidence to investigate and correct the implementation.
- Run the narrowest relevant formatting, test, and Release build commands, then expand verification in proportion to the risk and scope of the change.
- Report what changed, the verification performed, and any remaining risks or unverified assumptions.

3
CLAUDE.md

@ -0,0 +1,3 @@
# Claude Code Instructions
Read and follow [AGENTS.md](AGENTS.md) as the repository-wide source of coding, performance, and verification requirements. Apply any more-specific `AGENTS.md` or `CLAUDE.md` found below the files being changed.

4
Directory.Build.props

@ -21,10 +21,6 @@
<!-- Import the shared global .props file -->
<Import Project="$(MSBuildThisFileDirectory)shared-infrastructure\msbuild\props\SixLabors.Global.props" />
<PropertyGroup>
<LangVersion>12.0</LangVersion>
</PropertyGroup>
<!--
Ensure all custom build configurations based upon "Release" are optimized.
This is easier than setting each project individually.

3
GEMINI.md

@ -0,0 +1,3 @@
# Gemini CLI Instructions
Read and follow [AGENTS.md](AGENTS.md) as the repository-wide source of coding, performance, and verification requirements. Apply any more-specific `AGENTS.md` or `GEMINI.md` found below the files being changed.

12
ImageSharp.sln

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
# Visual Studio Version 18
VisualStudioVersion = 18.5.11723.231 stable
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_root", "_root", "{C317F1B1-D75E-4C6D-83EB-80367343E0D7}"
ProjectSection(SolutionItems) = preProject
@ -45,6 +45,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp", "src\ImageShar
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{56801022-D71A-4FBE-BC5B-CBA08E2284EC}"
ProjectSection(SolutionItems) = preProject
tests\coverlet.runsettings = tests\coverlet.runsettings
tests\Directory.Build.props = tests\Directory.Build.props
tests\Directory.Build.targets = tests\Directory.Build.targets
tests\ImageSharp.Tests.ruleset = tests\ImageSharp.Tests.ruleset
@ -555,6 +556,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Webp", "Webp", "{983A31E2-5
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp.Tests", "tests\ImageSharp.Tests\ImageSharp.Tests.csproj", "{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp.PublicApi.Tests", "tests\ImageSharp.PublicApi.Tests\ImageSharp.PublicApi.Tests.csproj", "{7D89D21A-0F54-4B36-BEAA-2B90994E840E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp.Benchmarks", "tests\ImageSharp.Benchmarks\ImageSharp.Benchmarks.csproj", "{2BF743D8-2A06-412D-96D7-F448F00C5EA5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{C0D7754B-5277-438E-ABEB-2BA34401B5A7}"
@ -694,6 +697,10 @@ Global
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{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
{7D89D21A-0F54-4B36-BEAA-2B90994E840E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7D89D21A-0F54-4B36-BEAA-2B90994E840E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7D89D21A-0F54-4B36-BEAA-2B90994E840E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7D89D21A-0F54-4B36-BEAA-2B90994E840E}.Release|Any CPU.Build.0 = Release|Any CPU
{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}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -726,6 +733,7 @@ Global
{E1C42A6F-913B-4A7B-B1A8-2BB62843B254} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
{983A31E2-5E26-4058-BD6E-03B4922D4BBF} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
{EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
{7D89D21A-0F54-4B36-BEAA-2B90994E840E} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
{2BF743D8-2A06-412D-96D7-F448F00C5EA5} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
{C0D7754B-5277-438E-ABEB-2BA34401B5A7} = {1799C43E-5C54-4A8F-8D64-B1475241DB0D}
{68A8CC40-6AED-4E96-B524-31B1158FDEEA} = {815C0625-CD3D-440F-9F80-2D83856AB7AE}

12
README.md

@ -8,18 +8,16 @@ SixLabors.ImageSharp
<div align="center">
[![Build Status](https://img.shields.io/github/actions/workflow/status/SixLabors/ImageSharp/build-and-test.yml?branch=main)](https://github.com/SixLabors/ImageSharp/actions)
[![Code coverage](https://codecov.io/gh/SixLabors/ImageSharp/branch/main/graph/badge.svg)](https://codecov.io/gh/SixLabors/ImageSharp)
[![codecov](https://codecov.io/gh/SixLabors/ImageSharp/graph/badge.svg?token=g2WJwz770q)](https://codecov.io/gh/SixLabors/ImageSharp)
[![License: Six Labors Split](https://img.shields.io/badge/license-Six%20Labors%20Split-%23e30183)](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE)
[![Twitter](https://img.shields.io/twitter/url/http/shields.io.svg?style=flat&logo=twitter)](https://twitter.com/intent/tweet?hashtags=imagesharp,dotnet,oss&text=ImageSharp.+A+new+cross-platform+2D+graphics+API+in+C%23&url=https%3a%2f%2fgithub.com%2fSixLabors%2fImageSharp&via=sixlabors)
</div>
### **ImageSharp** is a new, fully featured, fully managed, cross-platform, 2D graphics API.
### **ImageSharp** is a high-performance, fully managed, cross-platform 2D graphics API.
ImageSharp is a new, fully featured, fully managed, cross-platform, 2D graphics library.
Designed to simplify image processing, ImageSharp brings you an incredibly powerful yet beautifully simple API.
ImageSharp is a mature, fully featured, high-performance image processing and graphics library for .NET, built for workloads across device, cloud, and embedded/IoT scenarios.
ImageSharp is designed from the ground up to be flexible and extensible. The library provides API endpoints for common image processing operations and the building blocks to allow for the development of additional operations.
Designed from the ground up to balance performance, portability, and ease of use, ImageSharp provides a powerful yet approachable API for common image processing tasks, along with the low-level building blocks needed to extend the library for specialized workflows.
Built against [.NET 8](https://docs.microsoft.com/en-us/dotnet/standard/net-standard), ImageSharp can be used in device, cloud, and embedded/IoT scenarios.
@ -38,7 +36,7 @@ Support the efforts of the development of the Six Labors projects.
## Documentation
- [Detailed documentation](https://sixlabors.github.io/docs/) for the ImageSharp API is available. This includes additional conceptual documentation to help you get started.
- Our [Samples Repository](https://github.com/SixLabors/Samples/tree/main/ImageSharp) is also available containing buildable code samples demonstrating common activities.
- Our [Samples Repository](https://github.com/SixLabors/Samples/tree/main/SixLabors.Samples.ImageSharp) is also available containing buildable code samples demonstrating common activities.
## Questions

96
SECURITY.md

@ -0,0 +1,96 @@
# Security Policy
## Supported Versions
Six Labors provides security fixes only for the latest major version of each library.
Older major versions are end-of-life and do not receive security fixes.
Users must upgrade to the latest major version to receive security fixes.
| Version | Supported |
| -------------------- | --------- |
| Latest major version | Yes |
| Older major versions | No |
Security fixes, if any, are provided at Six Labors' discretion.
This policy does not create any obligation to provide support, maintenance services, SLAs, custom fixes, hosted services, managed services, operational monitoring, professional services, consulting, or certification of customer products.
## Reporting a Vulnerability
Please report suspected security vulnerabilities using GitHub private vulnerability reporting for the relevant Six Labors repository, where available.
If GitHub private vulnerability reporting is not available for a repository, please report suspected security vulnerabilities by contacting Six Labors through the contact details published on the Six Labors website.
Do not report security vulnerabilities through public GitHub issues.
When reporting a vulnerability, please include as much relevant information as possible:
* affected package and version
* target framework and runtime
* operating system
* input file or minimal reproduction, if safe to share
* expected and actual behavior
* potential security impact
* whether you believe the issue is being actively exploited
Six Labors may review reported vulnerabilities and determine whether they are security issues affecting a supported version.
A report may be declined or closed without action if, in Six Labors' opinion, it:
* is not reproducible
* does not affect a supported version
* affects only an unsupported or end-of-life version
* is not a security vulnerability
* depends on unsafe, unsupported, or unintended use
* depends on a vulnerable application, environment, dependency, configuration, or deployment outside the Six Labors library itself
* lacks sufficient information for assessment
* is duplicative
* has already been fixed
* is otherwise outside the scope of this policy
If a vulnerability is accepted, Six Labors may handle it through GitHub Security Advisories and, where appropriate, CVE assignment.
Six Labors does not guarantee any response time, fix time, release date, advisory publication date, CVE assignment, workaround, mitigation, or particular outcome for any report.
## Scope
This policy applies only to security vulnerabilities in Six Labors libraries themselves.
This policy does not apply to:
* customer applications
* customer products
* customer deployments
* customer infrastructure
* customer data
* third-party services
* unsupported versions
* end-of-life versions
* forks or modified versions
* usage outside the documented or intended behavior of the relevant library
Organizations using Six Labors libraries are responsible for assessing, securing, testing, monitoring, updating, and maintaining their own applications, products, deployments, infrastructure, and supply chains.
## Cyber Resilience Act
Six Labors libraries are general-purpose software libraries.
They are not cybersecurity products, identity or access management systems, password managers, operating systems, browsers, firewalls, network management tools, SIEM tools, hypervisors, container runtimes, or other Cyber Resilience Act important or critical product classes.
If a Six Labors library is treated as a product with digital elements under the Cyber Resilience Act, Six Labors assesses it as an ordinary software component.
Organizations incorporating Six Labors libraries into products made available on the EU market are responsible for assessing and meeting their own regulatory obligations for those products, including any obligations under the Cyber Resilience Act.
Six Labors does not provide support, maintenance services, SLAs, managed services, hosted services, operational monitoring, custom fixes, professional services, consulting, or certification of customer products.
Security vulnerabilities in supported Six Labors libraries are handled through the GitHub Security Advisory process for the relevant repository, where appropriate.
From 11 September 2026, if Six Labors becomes aware of credible active exploitation of a vulnerability in a supported Six Labors library, or a severe security incident affecting a supported Six Labors library, Six Labors may report the matter through the applicable Cyber Resilience Act reporting mechanism where legally required.
## No Warranty
Six Labors libraries are provided in accordance with their applicable license terms.
Nothing in this policy creates any warranty, representation, guarantee, support obligation, maintenance obligation, service commitment, regulatory certification, or assumption of responsibility for any customer product, customer deployment, customer compliance obligation, or third-party system.

18
global.json

@ -0,0 +1,18 @@
// Pins the .NET SDK to major version 10 or later for all SixLabors repositories.
// The build copies this file to consuming repositories together with .editorconfig and
// the other shared config files (see msbuild/targets/SixLabors.Src.targets).
{
"sdk": {
// The floor, not an exact build: resolution never selects an SDK below major 10.
"version": "10.0.0",
// Select the newest installed SDK at or above the floor. This spans feature bands,
// minor versions, and major versions, so a machine with only stable 10.0.x SDKs
// resolves the newest of those, and a machine with a newer SDK resolves that instead.
"rollForward": "latestMajor",
// Let the CLI and Visual Studio select preview SDKs when one is installed.
// Machines and CI lanes without a preview SDK are unaffected.
"allowPrerelease": true
}
}

2
shared-infrastructure

@ -1 +1 @@
Subproject commit d0f141bad2baf7e256aa38ef18129c31cfb857a5
Subproject commit 52fc1c3e87a755b2b17ce77e8cd22ec4bea17bcb

9
src/ImageSharp/Advanced/AdvancedImageExtensions.cs

@ -76,6 +76,15 @@ public static class AdvancedImageExtensions
public static Task AcceptVisitorAsync(this Image source, IImageVisitorAsync visitor, CancellationToken cancellationToken = default)
=> source.AcceptAsync(visitor, cancellationToken);
/// <summary>
/// Accepts a <see cref="IImageVisitor"/> to implement a double-dispatch pattern in order to
/// apply pixel-specific operations on non-generic <see cref="Image"/> instances
/// </summary>
/// <param name="source">The source image frame.</param>
/// <param name="visitor">The image visitor.</param>
public static void AcceptVisitor(this ImageFrame source, IImageFrameVisitor visitor)
=> source.Accept(visitor);
/// <summary>
/// Gets the representation of the pixels as a <see cref="IMemoryGroup{T}"/> containing the backing pixel data of the image
/// stored in row major order, as a list of contiguous <see cref="Memory{T}"/> blocks in the source image's pixel format.

179
src/ImageSharp/Advanced/AotCompilerTools.cs

@ -6,8 +6,12 @@ using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Ani;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Exr;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
@ -54,7 +58,7 @@ internal static class AotCompilerTools
/// <remarks>
/// This method doesn't actually do anything but serves an important purpose...
/// If you are running ImageSharp on iOS and try to call SaveAsGif, it will throw an exception:
/// "Attempting to JIT compile method... OctreeFrameQuantizer.ConstructPalette... while running in aot-only mode."
/// "Attempting to JIT compile method... HexadecatreeQuantizer.ConstructPalette... while running in aot-only mode."
/// The reason this happens is the SaveAsGif method makes heavy use of generics, which are too confusing for the AoT
/// compiler used on Xamarin.iOS. It spins up the JIT compiler to try and figure it out, but that is an illegal op on
/// iOS so it bombs out.
@ -81,10 +85,13 @@ internal static class AotCompilerTools
Seed<A8>();
Seed<Argb32>();
Seed<Argb32P>();
Seed<Abgr32>();
Seed<Abgr32P>();
Seed<Bgr24>();
Seed<Bgr565>();
Seed<Bgra32>();
Seed<Bgra32P>();
Seed<Bgra4444>();
Seed<Bgra5551>();
Seed<Byte4>();
@ -95,16 +102,23 @@ internal static class AotCompilerTools
Seed<HalfSingle>();
Seed<HalfVector2>();
Seed<HalfVector4>();
Seed<HalfVector4P>();
Seed<NormalizedByte2>();
Seed<NormalizedByte4>();
Seed<NormalizedByte4P>();
Seed<NormalizedShort2>();
Seed<NormalizedShort4>();
Seed<Rg32>();
Seed<Rgb24>();
Seed<Rgb48>();
Seed<Rgb96>();
Seed<Rgba1010102>();
Seed<Rgba128>();
Seed<Rgba32>();
Seed<Rgba32P>();
Seed<Rgba64>();
Seed<RgbaHalf>();
Seed<RgbaHalfP>();
Seed<RgbaVector>();
Seed<Short2>();
Seed<Short4>();
@ -117,6 +131,67 @@ internal static class AotCompilerTools
throw new InvalidOperationException("This method is used for AOT code generation only. Do not call it at runtime.");
}
/// <summary>
/// Seeds the modern .NET AOT compiler with bulk pixel operations for every built-in pixel format.
/// </summary>
/// <exception cref="InvalidOperationException">
/// This method is used for AOT code generation only. Do not call it at runtime.
/// </exception>
[Preserve]
public static void SeedPixelOperations()
{
try
{
// Keep this inventory finite and explicit. ImageSharp cannot precompile consumer-defined pixel types, while
// these closed calls make every built-in specialization visible without retaining the much larger legacy seed graph.
AotCompilePixelOperations<A8>();
AotCompilePixelOperations<Argb32>();
AotCompilePixelOperations<Argb32P>();
AotCompilePixelOperations<Abgr32>();
AotCompilePixelOperations<Abgr32P>();
AotCompilePixelOperations<Bgr24>();
AotCompilePixelOperations<Bgr565>();
AotCompilePixelOperations<Bgra32>();
AotCompilePixelOperations<Bgra32P>();
AotCompilePixelOperations<Bgra4444>();
AotCompilePixelOperations<Bgra5551>();
AotCompilePixelOperations<Byte4>();
AotCompilePixelOperations<L16>();
AotCompilePixelOperations<L8>();
AotCompilePixelOperations<La16>();
AotCompilePixelOperations<La32>();
AotCompilePixelOperations<HalfSingle>();
AotCompilePixelOperations<HalfVector2>();
AotCompilePixelOperations<HalfVector4>();
AotCompilePixelOperations<HalfVector4P>();
AotCompilePixelOperations<NormalizedByte2>();
AotCompilePixelOperations<NormalizedByte4>();
AotCompilePixelOperations<NormalizedByte4P>();
AotCompilePixelOperations<NormalizedShort2>();
AotCompilePixelOperations<NormalizedShort4>();
AotCompilePixelOperations<Rg32>();
AotCompilePixelOperations<Rgb24>();
AotCompilePixelOperations<Rgb48>();
AotCompilePixelOperations<Rgb96>();
AotCompilePixelOperations<Rgba1010102>();
AotCompilePixelOperations<Rgba128>();
AotCompilePixelOperations<Rgba32>();
AotCompilePixelOperations<Rgba32P>();
AotCompilePixelOperations<Rgba64>();
AotCompilePixelOperations<RgbaHalf>();
AotCompilePixelOperations<RgbaHalfP>();
AotCompilePixelOperations<RgbaVector>();
AotCompilePixelOperations<Short2>();
AotCompilePixelOperations<Short4>();
}
catch
{
// The calls only need to exist in IL; this method must never contribute a runtime execution path.
}
throw new InvalidOperationException("This method is used for AOT code generation only. Do not call it at runtime.");
}
/// <summary>
/// Seeds the compiler using the given pixel format.
/// </summary>
@ -127,6 +202,7 @@ internal static class AotCompilerTools
{
// This is we actually call all the individual methods you need to seed.
AotCompileImage<TPixel>();
AotCompilePixelOperations<TPixel>();
AotCompileImageProcessingContextFactory<TPixel>();
AotCompileImageEncoderInternals<TPixel>();
AotCompileImageDecoderInternals<TPixel>();
@ -147,21 +223,86 @@ internal static class AotCompilerTools
// TODO: Do the discovery work to figure out what works and what doesn't.
}
/// <summary>
/// Seeds the selected <see cref="PixelOperations{TPixel}"/> methods required by Mono WASM AOT.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
[Preserve]
private static void AotCompilePixelOperations<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
// These default arguments are never consumed. Direct calls are required so the IL contains the exact closed
// MethodSpecs that Mono WASM AOT can otherwise miss when following static-abstract pixel dispatch indirectly.
PixelOperations<TPixel> operations = PixelOperations<TPixel>.Instance;
_ = operations.GetPixelTypeInfo();
_ = operations.GetPixelBlender(default);
_ = operations.GetPixelBlender(default, default);
operations.FromVector4Destructive(default, default, default);
operations.FromVector4Destructive(default, default, default, default);
operations.ToVector4(default, default, default);
operations.ToVector4(default, default, default, default);
operations.PackFromRgbPlanes(default, default, default, default);
operations.UnpackIntoRgbPlanes(default, default, default, default);
operations.FromArgb32Bytes(default, default, default, default);
operations.ToArgb32Bytes(default, default, default, default);
operations.FromAbgr32Bytes(default, default, default, default);
operations.ToAbgr32Bytes(default, default, default, default);
operations.FromBgr24Bytes(default, default, default, default);
operations.ToBgr24Bytes(default, default, default, default);
operations.FromBgra32Bytes(default, default, default, default);
operations.ToBgra32Bytes(default, default, default, default);
operations.FromL8Bytes(default, default, default, default);
operations.ToL8Bytes(default, default, default, default);
operations.FromL16Bytes(default, default, default, default);
operations.ToL16Bytes(default, default, default, default);
operations.FromLa16Bytes(default, default, default, default);
operations.ToLa16Bytes(default, default, default, default);
operations.FromLa32Bytes(default, default, default, default);
operations.ToLa32Bytes(default, default, default, default);
operations.FromRgb24Bytes(default, default, default, default);
operations.ToRgb24Bytes(default, default, default, default);
operations.FromRgba32Bytes(default, default, default, default);
operations.ToRgba32Bytes(default, default, default, default);
operations.FromRgb48Bytes(default, default, default, default);
operations.ToRgb48Bytes(default, default, default, default);
operations.FromRgba64Bytes(default, default, default, default);
operations.ToRgba64Bytes(default, default, default, default);
operations.FromBgra5551Bytes(default, default, default, default);
operations.ToBgra5551Bytes(default, default, default, default);
}
/// <summary>
/// This method pre-seeds the <see cref="Image{TPixel}"/> for a given pixel format in the AoT compiler.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
[Preserve]
private static unsafe void AotCompileImage<TPixel>()
private static void AotCompileImage<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
Image<TPixel> img = default;
img.CloneAs<A8>(default);
img.CloneAs<Argb32>(default);
img.CloneAs<Argb32P>(default);
img.CloneAs<Abgr32>(default);
img.CloneAs<Abgr32P>(default);
img.CloneAs<Bgr24>(default);
img.CloneAs<Bgr565>(default);
img.CloneAs<Bgra32>(default);
img.CloneAs<Bgra32P>(default);
img.CloneAs<Bgra4444>(default);
img.CloneAs<Bgra5551>(default);
img.CloneAs<Byte4>(default);
@ -172,16 +313,23 @@ internal static class AotCompilerTools
img.CloneAs<HalfSingle>(default);
img.CloneAs<HalfVector2>(default);
img.CloneAs<HalfVector4>(default);
img.CloneAs<HalfVector4P>(default);
img.CloneAs<NormalizedByte2>(default);
img.CloneAs<NormalizedByte4>(default);
img.CloneAs<NormalizedByte4P>(default);
img.CloneAs<NormalizedShort2>(default);
img.CloneAs<NormalizedShort4>(default);
img.CloneAs<Rg32>(default);
img.CloneAs<Rgb24>(default);
img.CloneAs<Rgb48>(default);
img.CloneAs<Rgb96>(default);
img.CloneAs<Rgba1010102>(default);
img.CloneAs<Rgba128>(default);
img.CloneAs<Rgba32>(default);
img.CloneAs<Rgba32P>(default);
img.CloneAs<Rgba64>(default);
img.CloneAs<RgbaHalf>(default);
img.CloneAs<RgbaHalfP>(default);
img.CloneAs<RgbaVector>(default);
img.CloneAs<Short2>(default);
img.CloneAs<Short4>(default);
@ -207,8 +355,12 @@ internal static class AotCompilerTools
private static void AotCompileImageEncoderInternals<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
default(AniEncoderCore).Encode<TPixel>(default, default, default);
default(BmpEncoderCore).Encode<TPixel>(default, default, default);
default(CurEncoderCore).Encode<TPixel>(default, default, default);
default(ExrEncoderCore).Encode<TPixel>(default, default, default);
default(GifEncoderCore).Encode<TPixel>(default, default, default);
default(IcoEncoderCore).Encode<TPixel>(default, default, default);
default(JpegEncoderCore).Encode<TPixel>(default, default, default);
default(PbmEncoderCore).Encode<TPixel>(default, default, default);
default(PngEncoderCore).Encode<TPixel>(default, default, default);
@ -226,8 +378,12 @@ internal static class AotCompilerTools
private static void AotCompileImageDecoderInternals<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
default(AniDecoderCore).Decode<TPixel>(default, default, default);
default(BmpDecoderCore).Decode<TPixel>(default, default, default);
default(CurDecoderCore).Decode<TPixel>(default, default, default);
default(ExrDecoderCore).Decode<TPixel>(default, default, default);
default(GifDecoderCore).Decode<TPixel>(default, default, default);
default(IcoDecoderCore).Decode<TPixel>(default, default, default);
default(JpegDecoderCore).Decode<TPixel>(default, default, default);
default(PbmDecoderCore).Decode<TPixel>(default, default, default);
default(PngDecoderCore).Decode<TPixel>(default, default, default);
@ -247,10 +403,14 @@ internal static class AotCompilerTools
{
AotCompileImageEncoder<TPixel, WebpEncoder>();
AotCompileImageEncoder<TPixel, BmpEncoder>();
AotCompileImageEncoder<TPixel, CurEncoder>();
AotCompileImageEncoder<TPixel, ExrEncoder>();
AotCompileImageEncoder<TPixel, GifEncoder>();
AotCompileImageEncoder<TPixel, IcoEncoder>();
AotCompileImageEncoder<TPixel, JpegEncoder>();
AotCompileImageEncoder<TPixel, PbmEncoder>();
AotCompileImageEncoder<TPixel, PngEncoder>();
AotCompileImageEncoder<TPixel, QoiEncoder>();
AotCompileImageEncoder<TPixel, TgaEncoder>();
AotCompileImageEncoder<TPixel, TiffEncoder>();
}
@ -265,10 +425,14 @@ internal static class AotCompilerTools
{
AotCompileImageDecoder<TPixel, WebpDecoder>();
AotCompileImageDecoder<TPixel, BmpDecoder>();
AotCompileImageDecoder<TPixel, CurDecoder>();
AotCompileImageDecoder<TPixel, ExrDecoder>();
AotCompileImageDecoder<TPixel, GifDecoder>();
AotCompileImageDecoder<TPixel, IcoDecoder>();
AotCompileImageDecoder<TPixel, JpegDecoder>();
AotCompileImageDecoder<TPixel, PbmDecoder>();
AotCompileImageDecoder<TPixel, PngDecoder>();
AotCompileImageDecoder<TPixel, QoiDecoder>();
AotCompileImageDecoder<TPixel, TgaDecoder>();
AotCompileImageDecoder<TPixel, TiffDecoder>();
}
@ -336,6 +500,7 @@ internal static class AotCompilerTools
AotCompileImageProcessor<TPixel, VignetteProcessor>();
AotCompileImageProcessor<TPixel, AdaptiveHistogramEqualizationProcessor>();
AotCompileImageProcessor<TPixel, AdaptiveHistogramEqualizationSlidingWindowProcessor>();
AotCompileImageProcessor<TPixel, AutoLevelProcessor>();
AotCompileImageProcessor<TPixel, GlobalHistogramEqualizationProcessor>();
AotCompileImageProcessor<TPixel, AchromatomalyProcessor>();
AotCompileImageProcessor<TPixel, AchromatopsiaProcessor>();
@ -368,11 +533,13 @@ internal static class AotCompilerTools
AotCompileImageProcessor<TPixel, PaletteDitherProcessor>();
AotCompileImageProcessor<TPixel, BokehBlurProcessor>();
AotCompileImageProcessor<TPixel, BoxBlurProcessor>();
AotCompileImageProcessor<TPixel, ConvolutionProcessor>();
AotCompileImageProcessor<TPixel, EdgeDetector2DProcessor>();
AotCompileImageProcessor<TPixel, EdgeDetectorCompassProcessor>();
AotCompileImageProcessor<TPixel, EdgeDetectorProcessor>();
AotCompileImageProcessor<TPixel, GaussianBlurProcessor>();
AotCompileImageProcessor<TPixel, GaussianSharpenProcessor>();
AotCompileImageProcessor<TPixel, MedianBlurProcessor>();
AotCompileImageProcessor<TPixel, AdaptiveThresholdProcessor>();
AotCompileImageProcessor<TPixel, BinaryThresholdProcessor>();
@ -479,7 +646,7 @@ internal static class AotCompilerTools
private static void AotCompileQuantizers<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
AotCompileQuantizer<TPixel, OctreeQuantizer>();
AotCompileQuantizer<TPixel, HexadecatreeQuantizer>();
AotCompileQuantizer<TPixel, PaletteQuantizer>();
AotCompileQuantizer<TPixel, WebSafePaletteQuantizer>();
AotCompileQuantizer<TPixel, WernerPaletteQuantizer>();
@ -523,10 +690,8 @@ internal static class AotCompilerTools
private static void AotCompilePixelMaps<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
default(EuclideanPixelMap<TPixel, HybridCache>).GetClosestColor(default, out _);
default(EuclideanPixelMap<TPixel, AccurateCache>).GetClosestColor(default, out _);
default(EuclideanPixelMap<TPixel, CoarseCache>).GetClosestColor(default, out _);
default(EuclideanPixelMap<TPixel, NullCache>).GetClosestColor(default, out _);
}
/// <summary>
@ -551,8 +716,8 @@ internal static class AotCompilerTools
where TPixel : unmanaged, IPixel<TPixel>
where TDither : struct, IDither
{
OctreeQuantizer<TPixel> octree = default;
default(TDither).ApplyQuantizationDither<OctreeQuantizer<TPixel>, TPixel>(ref octree, default, default, default);
HexadecatreeQuantizer<TPixel> hexadecatree = default;
default(TDither).ApplyQuantizationDither<HexadecatreeQuantizer<TPixel>, TPixel>(ref hexadecatree, default, default, default);
PaletteQuantizer<TPixel> palette = default;
default(TDither).ApplyQuantizationDither<PaletteQuantizer<TPixel>, TPixel>(ref palette, default, default, default);

21
src/ImageSharp/Advanced/IImageFrameVisitor.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Advanced;
/// <summary>
/// A visitor to implement a double-dispatch pattern in order to apply pixel-specific operations
/// on non-generic <see cref="ImageFrame"/> instances.
/// </summary>
public interface IImageFrameVisitor
{
/// <summary>
/// Provides a pixel-specific implementation for a given operation.
/// </summary>
/// <param name="frame">The image frame.</param>
/// <typeparam name="TPixel">The pixel type.</typeparam>
public void Visit<TPixel>(ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel>;
}

4
src/ImageSharp/Advanced/IImageVisitor.cs

@ -16,7 +16,7 @@ public interface IImageVisitor
/// </summary>
/// <param name="image">The image.</param>
/// <typeparam name="TPixel">The pixel type.</typeparam>
void Visit<TPixel>(Image<TPixel> image)
public void Visit<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>;
}
@ -33,6 +33,6 @@ public interface IImageVisitorAsync
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <typeparam name="TPixel">The pixel type.</typeparam>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task VisitAsync<TPixel>(Image<TPixel> image, CancellationToken cancellationToken)
public Task VisitAsync<TPixel>(Image<TPixel> image, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>;
}

4
src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs

@ -15,12 +15,12 @@ public interface IRowOperation<TBuffer>
/// </summary>
/// <param name="bounds">The bounds of the operation.</param>
/// <returns>The required buffer length.</returns>
int GetRequiredBufferLength(Rectangle bounds);
public int GetRequiredBufferLength(Rectangle bounds);
/// <summary>
/// Invokes the method passing the row and a buffer.
/// </summary>
/// <param name="y">The row y coordinate.</param>
/// <param name="span">The contiguous region of memory.</param>
void Invoke(int y, Span<TBuffer> span);
public void Invoke(int y, Span<TBuffer> span);
}

19
src/ImageSharp/Advanced/ParallelExecutionSettings.cs

@ -18,7 +18,10 @@ public readonly struct ParallelExecutionSettings
/// <summary>
/// Initializes a new instance of the <see cref="ParallelExecutionSettings"/> struct.
/// </summary>
/// <param name="maxDegreeOfParallelism">The value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.</param>
/// <param name="maxDegreeOfParallelism">
/// The value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.
/// If set to <c>-1</c>, there is no limit on the number of concurrently running operations.
/// </param>
/// <param name="minimumPixelsProcessedPerTask">The value for <see cref="MinimumPixelsProcessedPerTask"/>.</param>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/>.</param>
public ParallelExecutionSettings(
@ -28,7 +31,7 @@ public readonly struct ParallelExecutionSettings
{
// Shall be compatible with ParallelOptions.MaxDegreeOfParallelism:
// https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.paralleloptions.maxdegreeofparallelism
if (maxDegreeOfParallelism == 0 || maxDegreeOfParallelism < -1)
if (maxDegreeOfParallelism is 0 or < -1)
{
throw new ArgumentOutOfRangeException(nameof(maxDegreeOfParallelism));
}
@ -44,7 +47,10 @@ public readonly struct ParallelExecutionSettings
/// <summary>
/// Initializes a new instance of the <see cref="ParallelExecutionSettings"/> struct.
/// </summary>
/// <param name="maxDegreeOfParallelism">The value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.</param>
/// <param name="maxDegreeOfParallelism">
/// The value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.
/// If set to <c>-1</c>, there is no limit on the number of concurrently running operations.
/// </param>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/>.</param>
public ParallelExecutionSettings(int maxDegreeOfParallelism, MemoryAllocator memoryAllocator)
: this(maxDegreeOfParallelism, DefaultMinimumPixelsProcessedPerTask, memoryAllocator)
@ -58,6 +64,7 @@ public readonly struct ParallelExecutionSettings
/// <summary>
/// Gets the value used for initializing <see cref="ParallelOptions.MaxDegreeOfParallelism"/> when using TPL.
/// A value of <c>-1</c> means there is no limit on the number of concurrently running operations.
/// </summary>
public int MaxDegreeOfParallelism { get; }
@ -86,12 +93,10 @@ public readonly struct ParallelExecutionSettings
}
/// <summary>
/// Get the default <see cref="SixLabors.ImageSharp.Advanced.ParallelExecutionSettings"/> for a <see cref="SixLabors.ImageSharp.Configuration"/>
/// Get the default <see cref="ParallelExecutionSettings"/> for a <see cref="Configuration"/>
/// </summary>
/// <param name="configuration">The <see cref="Configuration"/>.</param>
/// <returns>The <see cref="ParallelExecutionSettings"/>.</returns>
public static ParallelExecutionSettings FromConfiguration(Configuration configuration)
{
return new ParallelExecutionSettings(configuration.MaxDegreeOfParallelism, configuration.MemoryAllocator);
}
=> new(configuration.MaxDegreeOfParallelism, configuration.MemoryAllocator);
}

59
src/ImageSharp/Advanced/ParallelRowIterator.cs

@ -50,8 +50,7 @@ public static partial class ParallelRowIterator
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
int numOfSteps = GetNumberOfSteps(width, height, parallelSettings);
// Avoid TPL overhead in this trivial case:
if (numOfSteps == 1)
@ -65,10 +64,10 @@ public static partial class ParallelRowIterator
}
int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps);
RowOperationWrapper<T> wrappingOperation = new(top, bottom, verticalStep, in operation);
Parallel.For(
_ = Parallel.For(
0,
numOfSteps,
parallelOptions,
@ -115,8 +114,7 @@ public static partial class ParallelRowIterator
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
int numOfSteps = GetNumberOfSteps(width, height, parallelSettings);
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
int bufferLength = Unsafe.AsRef(in operation).GetRequiredBufferLength(rectangle);
@ -135,10 +133,10 @@ public static partial class ParallelRowIterator
}
int verticalStep = DivideCeil(height, numOfSteps);
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps);
RowOperationWrapper<T, TBuffer> wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
Parallel.For(
_ = Parallel.For(
0,
numOfSteps,
parallelOptions,
@ -180,8 +178,7 @@ public static partial class ParallelRowIterator
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
int numOfSteps = GetNumberOfSteps(width, height, parallelSettings);
// Avoid TPL overhead in this trivial case:
if (numOfSteps == 1)
@ -192,10 +189,10 @@ public static partial class ParallelRowIterator
}
int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps);
RowIntervalOperationWrapper<T> wrappingOperation = new(top, bottom, verticalStep, in operation);
Parallel.For(
_ = Parallel.For(
0,
numOfSteps,
parallelOptions,
@ -242,8 +239,7 @@ public static partial class ParallelRowIterator
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
int numOfSteps = GetNumberOfSteps(width, height, parallelSettings);
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
int bufferLength = Unsafe.AsRef(in operation).GetRequiredBufferLength(rectangle);
@ -259,10 +255,10 @@ public static partial class ParallelRowIterator
}
int verticalStep = DivideCeil(height, numOfSteps);
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
ParallelOptions parallelOptions = CreateParallelOptions(parallelSettings, numOfSteps);
RowIntervalOperationWrapper<T, TBuffer> wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
Parallel.For(
_ = Parallel.For(
0,
numOfSteps,
parallelOptions,
@ -272,6 +268,37 @@ public static partial class ParallelRowIterator
[MethodImpl(InliningOptions.ShortMethod)]
private static int DivideCeil(long dividend, int divisor) => (int)Math.Min(1 + ((dividend - 1) / divisor), int.MaxValue);
/// <summary>
/// Creates the <see cref="ParallelOptions"/> for the current iteration.
/// </summary>
/// <param name="parallelSettings">The execution settings.</param>
/// <param name="numOfSteps">The number of row partitions to execute.</param>
/// <returns>The <see cref="ParallelOptions"/> instance.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
private static ParallelOptions CreateParallelOptions(in ParallelExecutionSettings parallelSettings, int numOfSteps)
=> new() { MaxDegreeOfParallelism = parallelSettings.MaxDegreeOfParallelism == -1 ? -1 : numOfSteps };
/// <summary>
/// Calculates the number of row partitions to execute for the given region.
/// </summary>
/// <param name="width">The width of the region.</param>
/// <param name="height">The height of the region.</param>
/// <param name="parallelSettings">The execution settings.</param>
/// <returns>The number of row partitions to execute.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
private static int GetNumberOfSteps(int width, int height, in ParallelExecutionSettings parallelSettings)
{
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
if (parallelSettings.MaxDegreeOfParallelism == -1)
{
// Row batching cannot produce more useful partitions than the number of rows available.
return Math.Min(height, maxSteps);
}
return Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
}
private static void ValidateRectangle(Rectangle rectangle)
{
Guard.MustBeGreaterThan(

6
src/ImageSharp/Color/Color.WernerPalette.cs

@ -127,6 +127,10 @@ public partial struct Color
ParseHex("#8b7859"),
ParseHex("#9b856b"),
ParseHex("#766051"),
ParseHex("#453b32")
ParseHex("#453b32"),
// Werner does not define a transparent color, but we need to add one to
// make the palette work with the rest of the library.
Transparent
];
}

199
src/ImageSharp/Color/Color.cs

@ -21,29 +21,44 @@ public readonly partial struct Color : IEquatable<Color>
{
private readonly Vector4 data;
private readonly IPixel? boxedHighPrecisionPixel;
private readonly bool isAssociated;
private readonly bool dataIsAssociated;
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct.
/// </summary>
/// <param name="vector">The <see cref="Vector4"/> containing the color information.</param>
/// <param name="alphaRepresentation">The alpha representation exposed by the color.</param>
/// <param name="dataAlphaRepresentation">The alpha representation of <paramref name="vector"/>.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Color(Vector4 vector)
private Color(Vector4 vector, PixelAlphaRepresentation alphaRepresentation, PixelAlphaRepresentation dataAlphaRepresentation)
{
this.data = Numerics.Clamp(vector, Vector4.Zero, Vector4.One);
this.boxedHighPrecisionPixel = null;
this.isAssociated = alphaRepresentation == PixelAlphaRepresentation.Associated;
this.dataIsAssociated = dataAlphaRepresentation == PixelAlphaRepresentation.Associated;
}
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct.
/// </summary>
/// <param name="pixel">The pixel containing color information.</param>
/// <param name="alphaRepresentation">The alpha representation of <paramref name="pixel"/>.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Color(IPixel pixel)
private Color(IPixel pixel, PixelAlphaRepresentation alphaRepresentation)
{
this.boxedHighPrecisionPixel = pixel;
this.data = default;
this.isAssociated = alphaRepresentation == PixelAlphaRepresentation.Associated;
this.dataIsAssociated = this.isAssociated;
}
/// <summary>
/// Gets the alpha representation used by this color's scaled vector.
/// </summary>
public PixelAlphaRepresentation AlphaRepresentation
=> this.isAssociated ? PixelAlphaRepresentation.Associated : PixelAlphaRepresentation.Unassociated;
/// <summary>
/// Checks whether two <see cref="Color"/> structures are equal.
/// </summary>
@ -80,21 +95,73 @@ public readonly partial struct Color : IEquatable<Color>
{
// Avoid boxing in case we can convert to Vector4 safely and efficiently
PixelTypeInfo info = TPixel.GetPixelTypeInfo();
if (info.ComponentInfo.HasValue && info.ComponentInfo.Value.GetMaximumComponentPrecision() <= (int)PixelComponentBitDepth.Bit32)
if (info.ComponentInfo.HasValue)
{
return new Color(source.ToScaledVector4());
int maximumComponentPrecision = info.ComponentInfo.Value.GetMaximumComponentPrecision();
if (maximumComponentPrecision <= (int)PixelComponentBitDepth.Bit32)
{
if (info.AlphaRepresentation == PixelAlphaRepresentation.Associated && maximumComponentPrecision <= (int)PixelComponentBitDepth.Bit8)
{
// Associated formats with at most eight bits per component can be canonicalized without loss by their pixel-specific conversion.
// Higher-precision formats retain their associated values because unassociation can lose representable data.
Vector4 vector = source.ToUnassociatedScaledVector4();
return new Color(vector, info.AlphaRepresentation, PixelAlphaRepresentation.Unassociated);
}
return new Color(source.ToScaledVector4(), info.AlphaRepresentation, info.AlphaRepresentation);
}
}
return new Color(source);
return new Color(source, info.AlphaRepresentation);
}
/// <summary>
/// Creates a <see cref="Color"/> from a generic scaled <see cref="Vector4"/>.
/// </summary>
/// <param name="source">The vector to load the pixel from.</param>
/// <param name="source">The unassociated vector to load the color from.</param>
/// <returns>The <see cref="Color"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Color FromScaledVector(Vector4 source) => new(source);
public static Color FromScaledVector(Vector4 source)
=> new(source, PixelAlphaRepresentation.Unassociated, PixelAlphaRepresentation.Unassociated);
/// <summary>
/// Creates a <see cref="Color"/> from a generic scaled <see cref="Vector4"/> with the specified alpha representation.
/// </summary>
/// <param name="source">The vector to load the color from.</param>
/// <param name="alphaRepresentation">The alpha representation of <paramref name="source"/>.</param>
/// <returns>The <see cref="Color"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Color FromScaledVector(Vector4 source, PixelAlphaRepresentation alphaRepresentation)
=> new(source, alphaRepresentation, alphaRepresentation);
/// <summary>
/// Bulk converts a span of generic scaled <see cref="Vector4"/> to a span of <see cref="Color"/>.
/// </summary>
/// <param name="source">The source vector span.</param>
/// <param name="destination">The destination color span.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void FromScaledVector(ReadOnlySpan<Vector4> source, Span<Color> destination)
=> FromScaledVector(source, destination, PixelAlphaRepresentation.Unassociated);
/// <summary>
/// Bulk converts a span of generic scaled <see cref="Vector4"/> values with the specified alpha representation
/// to a span of <see cref="Color"/> values.
/// </summary>
/// <param name="source">The source vector span.</param>
/// <param name="destination">The destination color span.</param>
/// <param name="alphaRepresentation">The alpha representation of the source vectors.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void FromScaledVector(ReadOnlySpan<Vector4> source, Span<Color> destination, PixelAlphaRepresentation alphaRepresentation)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
destination[i] = FromScaledVector(source[i], alphaRepresentation);
}
}
/// <summary>
/// Bulk converts a span of a specified <typeparamref name="TPixel"/> type to a span of <see cref="Color"/>.
@ -110,19 +177,37 @@ public readonly partial struct Color : IEquatable<Color>
// Avoid boxing in case we can convert to Vector4 safely and efficiently
PixelTypeInfo info = TPixel.GetPixelTypeInfo();
if (info.ComponentInfo.HasValue && info.ComponentInfo.Value.GetMaximumComponentPrecision() <= (int)PixelComponentBitDepth.Bit32)
if (info.ComponentInfo.HasValue)
{
for (int i = 0; i < destination.Length; i++)
int maximumComponentPrecision = info.ComponentInfo.Value.GetMaximumComponentPrecision();
if (maximumComponentPrecision <= (int)PixelComponentBitDepth.Bit32)
{
destination[i] = FromScaledVector(source[i].ToScaledVector4());
if (info.AlphaRepresentation == PixelAlphaRepresentation.Associated && maximumComponentPrecision <= (int)PixelComponentBitDepth.Bit8)
{
// Match the scalar conversion by retaining exact unassociated values from the format-specific operation.
for (int i = 0; i < source.Length; i++)
{
Vector4 vector = source[i].ToUnassociatedScaledVector4();
destination[i] = new Color(vector, info.AlphaRepresentation, PixelAlphaRepresentation.Unassociated);
}
return;
}
for (int i = 0; i < source.Length; i++)
{
destination[i] = new Color(source[i].ToScaledVector4(), info.AlphaRepresentation, info.AlphaRepresentation);
}
return;
}
}
else
for (int i = 0; i < source.Length; i++)
{
for (int i = 0; i < destination.Length; i++)
{
destination[i] = new Color(source[i]);
}
destination[i] = new Color(source[i], info.AlphaRepresentation);
}
}
@ -261,13 +346,13 @@ public readonly partial struct Color : IEquatable<Color>
/// Alters the alpha channel of the color, returning a new instance.
/// </summary>
/// <param name="alpha">The new value of alpha [0..1].</param>
/// <returns>The color having it's alpha channel altered.</returns>
/// <returns>The color having its alpha channel altered.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Color WithAlpha(float alpha)
{
Vector4 v = this.ToScaledVector4();
v.W = alpha;
return FromScaledVector(v);
Vector4 vector = this.ToScaledVector4(PixelAlphaRepresentation.Unassociated);
vector.W = Numerics.Clamp(alpha, 0, 1);
return new Color(vector, this.AlphaRepresentation, PixelAlphaRepresentation.Unassociated);
}
/// <summary>
@ -281,9 +366,7 @@ public readonly partial struct Color : IEquatable<Color>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string ToHex(ColorHexFormat format = ColorHexFormat.Rgba)
{
Rgba32 rgba = (this.boxedHighPrecisionPixel is not null)
? this.boxedHighPrecisionPixel.ToRgba32()
: Rgba32.FromScaledVector4(this.data);
Rgba32 rgba = this.ToPixel<Rgba32>();
uint hexOrder = format switch
{
@ -312,29 +395,76 @@ public readonly partial struct Color : IEquatable<Color>
return pixel;
}
if (this.boxedHighPrecisionPixel is null)
Vector4 vector = this.boxedHighPrecisionPixel?.ToScaledVector4() ?? this.data;
if (this.dataIsAssociated)
{
return TPixel.FromScaledVector4(this.data);
// Preserve associated components directly while allowing the destination to quantize alpha to its own storage grid.
return TPixel.FromAssociatedScaledVector4(vector);
}
return TPixel.FromScaledVector4(this.boxedHighPrecisionPixel.ToScaledVector4());
// Unassociated input lets an associated destination quantize alpha before it multiplies the color components.
return TPixel.FromUnassociatedScaledVector4(vector);
}
/// <summary>
/// Expands the color into a generic ("scaled") <see cref="Vector4"/> representation
/// with values scaled and clamped between <value>0</value> and <value>1</value>.
/// Expands the color into a generic ("scaled") <see cref="Vector4"/> representation,
/// preserving the <see cref="AlphaRepresentation"/>, with values scaled and clamped between
/// <value>0</value> and <value>1</value>.
/// The vector components are typically expanded in least to greatest significance order.
/// </summary>
/// <returns>The <see cref="Vector4"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ToScaledVector4()
{
if (this.boxedHighPrecisionPixel is null)
Vector4 vector = this.boxedHighPrecisionPixel?.ToScaledVector4() ?? this.data;
if (this.dataIsAssociated == this.isAssociated)
{
return vector;
}
if (this.isAssociated)
{
Numerics.Premultiply(ref vector);
}
else
{
Numerics.UnPremultiply(ref vector);
}
return vector;
}
/// <summary>
/// Expands the color into a generic ("scaled") <see cref="Vector4"/> using the specified alpha representation,
/// with values scaled and clamped between <value>0</value> and <value>1</value>.
/// </summary>
/// <param name="alphaRepresentation">
/// The alpha representation to apply. <see cref="PixelAlphaRepresentation.Associated"/> returns color components
/// multiplied by alpha; other representations return color components independent of alpha.
/// </param>
/// <returns>The <see cref="Vector4"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ToScaledVector4(PixelAlphaRepresentation alphaRepresentation)
{
bool targetIsAssociated = alphaRepresentation == PixelAlphaRepresentation.Associated;
Vector4 vector = this.boxedHighPrecisionPixel?.ToScaledVector4() ?? this.data;
if (this.dataIsAssociated == targetIsAssociated)
{
return vector;
}
if (targetIsAssociated)
{
Numerics.Premultiply(ref vector);
}
else
{
return this.data;
Numerics.UnPremultiply(ref vector);
}
return this.boxedHighPrecisionPixel.ToScaledVector4();
return vector;
}
/// <summary>
@ -362,10 +492,11 @@ public readonly partial struct Color : IEquatable<Color>
{
if (this.boxedHighPrecisionPixel is null && other.boxedHighPrecisionPixel is null)
{
return this.data == other.data;
return this.isAssociated == other.isAssociated && this.ToScaledVector4() == other.ToScaledVector4();
}
return this.boxedHighPrecisionPixel?.Equals(other.boxedHighPrecisionPixel) == true;
return this.isAssociated == other.isAssociated
&& this.boxedHighPrecisionPixel?.Equals(other.boxedHighPrecisionPixel) == true;
}
/// <inheritdoc />
@ -377,10 +508,10 @@ public readonly partial struct Color : IEquatable<Color>
{
if (this.boxedHighPrecisionPixel is null)
{
return this.data.GetHashCode();
return HashCode.Combine(this.ToScaledVector4(), this.isAssociated);
}
return this.boxedHighPrecisionPixel.GetHashCode();
return HashCode.Combine(this.boxedHighPrecisionPixel.ToScaledVector4(), this.isAssociated);
}
/// <summary>

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs

@ -6,8 +6,24 @@ using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsCieLabCieLab
/// <summary>
/// Allows conversion between two color profiles based on the CIE Lab color space.
/// </summary>
public static class ColorProfileConverterExtensionsCieLabCieLab
{
/// <summary>
/// Converts a color value from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// The conversion process may use ICC profiles if available; otherwise, it performs a manual
/// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires
/// both source and target types to be value types implementing the appropriate color profile interface.
/// </remarks>
/// <typeparam name="TFrom">The source color profile type. Must implement <see cref="IColorProfile{TFrom, CieLab}"/>.</typeparam>
/// <typeparam name="TTo">The target color profile type. Must implement <see cref="IColorProfile{TTo, CieLab}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion.</param>
/// <param name="source">The source color value to convert.</param>
/// <returns>A value of type <typeparamref name="TTo"/> representing the converted color in the target color profile.</returns>
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
where TFrom : struct, IColorProfile<TFrom, CieLab>
where TTo : struct, IColorProfile<TTo, CieLab>
@ -34,6 +50,20 @@ internal static class ColorProfileConverterExtensionsCieLabCieLab
return TTo.FromProfileConnectingSpace(options, in pcsTo);
}
/// <summary>
/// Converts a span of color values from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// This method performs color conversion between two color profiles, handling necessary
/// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are
/// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory
/// for the destination; the caller is responsible for providing a suitably sized span.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom, CieLab}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo, CieLab}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion operation.</param>
/// <param name="source">A read-only span containing the source color values to convert.</param>
/// <param name="destination">A span that receives the converted color values. Must be at least as long as the source span.</param>
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
where TFrom : struct, IColorProfile<TFrom, CieLab>
where TTo : struct, IColorProfile<TTo, CieLab>

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs

@ -6,8 +6,24 @@ using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsCieLabCieXyz
/// <summary>
/// Allows conversion between two color profiles based on the CIE Lab and CIE XYZ color spaces.
/// </summary>
public static class ColorProfileConverterExtensionsCieLabCieXyz
{
/// <summary>
/// Converts a color value from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// The conversion process may use ICC profiles if available; otherwise, it performs a manual
/// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires
/// both source and target types to be value types implementing the appropriate color profile interface.
/// </remarks>
/// <typeparam name="TFrom">The source color profile type. Must implement <see cref="IColorProfile{TFrom, CieLab}"/>.</typeparam>
/// <typeparam name="TTo">The target color profile type. Must implement <see cref="IColorProfile{TTo, CieXyz}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion.</param>
/// <param name="source">The source color value to convert.</param>
/// <returns>A value of type <typeparamref name="TTo"/> representing the converted color in the target color profile.</returns>
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
where TFrom : struct, IColorProfile<TFrom, CieLab>
where TTo : struct, IColorProfile<TTo, CieXyz>
@ -33,6 +49,20 @@ internal static class ColorProfileConverterExtensionsCieLabCieXyz
return TTo.FromProfileConnectingSpace(options, in pcsTo);
}
/// <summary>
/// Converts a span of color values from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// This method performs color conversion between two color profiles, handling necessary
/// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are
/// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory
/// for the destination; the caller is responsible for providing a suitably sized span.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom, CieLab}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo, CieXyz}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion operation.</param>
/// <param name="source">A read-only span containing the source color values to convert.</param>
/// <param name="destination">A span that receives the converted color values. Must be at least as long as the source span.</param>
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
where TFrom : struct, IColorProfile<TFrom, CieLab>
where TTo : struct, IColorProfile<TTo, CieXyz>

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs

@ -6,8 +6,24 @@ using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsCieLabRgb
/// <summary>
/// Allows conversion between two color profiles based on the CIE Lab and RGB color spaces.
/// </summary>
public static class ColorProfileConverterExtensionsCieLabRgb
{
/// <summary>
/// Converts a color value from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// The conversion process may use ICC profiles if available; otherwise, it performs a manual
/// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires
/// both source and target types to be value types implementing the appropriate color profile interface.
/// </remarks>
/// <typeparam name="TFrom">The source color profile type. Must implement <see cref="IColorProfile{TFrom, CieLab}"/>.</typeparam>
/// <typeparam name="TTo">The target color profile type. Must implement <see cref="IColorProfile{TTo, Rgb}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion.</param>
/// <param name="source">The source color value to convert.</param>
/// <returns>A value of type <typeparamref name="TTo"/> representing the converted color in the target color profile.</returns>
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
where TFrom : struct, IColorProfile<TFrom, CieLab>
where TTo : struct, IColorProfile<TTo, Rgb>
@ -34,6 +50,20 @@ internal static class ColorProfileConverterExtensionsCieLabRgb
return TTo.FromProfileConnectingSpace(options, in pcsTo);
}
/// <summary>
/// Converts a span of color values from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// This method performs color conversion between two color profiles, handling necessary
/// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are
/// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory
/// for the destination; the caller is responsible for providing a suitably sized span.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom, CieLab}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo, Rgb}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion operation.</param>
/// <param name="source">A read-only span containing the source color values to convert.</param>
/// <param name="destination">A span that receives the converted color values. Must be at least as long as the source span.</param>
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
where TFrom : struct, IColorProfile<TFrom, CieLab>
where TTo : struct, IColorProfile<TTo, Rgb>

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs

@ -6,8 +6,24 @@ using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsCieXyzCieLab
/// <summary>
/// Allows conversion between two color profiles based on the CIE XYZ and CIE Lab color spaces.
/// </summary>
public static class ColorProfileConverterExtensionsCieXyzCieLab
{
/// <summary>
/// Converts a color value from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// The conversion process may use ICC profiles if available; otherwise, it performs a manual
/// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires
/// both source and target types to be value types implementing the appropriate color profile interface.
/// </remarks>
/// <typeparam name="TFrom">The source color profile type. Must implement <see cref="IColorProfile{TFrom, CieXyz}"/>.</typeparam>
/// <typeparam name="TTo">The target color profile type. Must implement <see cref="IColorProfile{TTo, CieLab}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion.</param>
/// <param name="source">The source color value to convert.</param>
/// <returns>A value of type <typeparamref name="TTo"/> representing the converted color in the target color profile.</returns>
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
where TFrom : struct, IColorProfile<TFrom, CieXyz>
where TTo : struct, IColorProfile<TTo, CieLab>
@ -33,6 +49,20 @@ internal static class ColorProfileConverterExtensionsCieXyzCieLab
return TTo.FromProfileConnectingSpace(options, in pcsTo);
}
/// <summary>
/// Converts a span of color values from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// This method performs color conversion between two color profiles, handling necessary
/// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are
/// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory
/// for the destination; the caller is responsible for providing a suitably sized span.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom, CieXyz}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo, CieLab}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion operation.</param>
/// <param name="source">A read-only span containing the source color values to convert.</param>
/// <param name="destination">A span that receives the converted color values. Must be at least as long as the source span.</param>
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
where TFrom : struct, IColorProfile<TFrom, CieXyz>
where TTo : struct, IColorProfile<TTo, CieLab>

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs

@ -6,8 +6,24 @@ using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsCieXyzCieXyz
/// <summary>
/// Allows conversion between two color profiles based on the CIE XYZ color space.
/// </summary>
public static class ColorProfileConverterExtensionsCieXyzCieXyz
{
/// <summary>
/// Converts a color value from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// The conversion process may use ICC profiles if available; otherwise, it performs a manual
/// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires
/// both source and target types to be value types implementing the appropriate color profile interface.
/// </remarks>
/// <typeparam name="TFrom">The source color profile type. Must implement <see cref="IColorProfile{TFrom, CieXyz}"/>.</typeparam>
/// <typeparam name="TTo">The target color profile type. Must implement <see cref="IColorProfile{TTo, CieXyz}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion.</param>
/// <param name="source">The source color value to convert.</param>
/// <returns>A value of type <typeparamref name="TTo"/> representing the converted color in the target color profile.</returns>
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
where TFrom : struct, IColorProfile<TFrom, CieXyz>
where TTo : struct, IColorProfile<TTo, CieXyz>
@ -30,6 +46,20 @@ internal static class ColorProfileConverterExtensionsCieXyzCieXyz
return TTo.FromProfileConnectingSpace(options, in pcsFrom);
}
/// <summary>
/// Converts a span of color values from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// This method performs color conversion between two color profiles, handling necessary
/// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are
/// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory
/// for the destination; the caller is responsible for providing a suitably sized span.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom, CieXyz}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo, CieXyz}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion operation.</param>
/// <param name="source">A read-only span containing the source color values to convert.</param>
/// <param name="destination">A span that receives the converted color values. Must be at least as long as the source span.</param>
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
where TFrom : struct, IColorProfile<TFrom, CieXyz>
where TTo : struct, IColorProfile<TTo, CieXyz>

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs

@ -6,8 +6,24 @@ using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsCieXyzRgb
/// <summary>
/// Allows conversion between two color profiles based on the CIE XYZ and RGB color spaces.
/// </summary>
public static class ColorProfileConverterExtensionsCieXyzRgb
{
/// <summary>
/// Converts a color value from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// The conversion process may use ICC profiles if available; otherwise, it performs a manual
/// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires
/// both source and target types to be value types implementing the appropriate color profile interface.
/// </remarks>
/// <typeparam name="TFrom">The source color profile type. Must implement <see cref="IColorProfile{TFrom, CieXyz}"/>.</typeparam>
/// <typeparam name="TTo">The target color profile type. Must implement <see cref="IColorProfile{TTo, Rgb}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion.</param>
/// <param name="source">The source color value to convert.</param>
/// <returns>A value of type <typeparamref name="TTo"/> representing the converted color in the target color profile.</returns>
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
where TFrom : struct, IColorProfile<TFrom, CieXyz>
where TTo : struct, IColorProfile<TTo, Rgb>
@ -33,6 +49,20 @@ internal static class ColorProfileConverterExtensionsCieXyzRgb
return TTo.FromProfileConnectingSpace(options, in pcsTo);
}
/// <summary>
/// Converts a span of color values from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// This method performs color conversion between two color profiles, handling necessary
/// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are
/// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory
/// for the destination; the caller is responsible for providing a suitably sized span.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom, CieXyz}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo, Rgb}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion operation.</param>
/// <param name="source">A read-only span containing the source color values to convert.</param>
/// <param name="destination">A span that receives the converted color values. Must be at least as long as the source span.</param>
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
where TFrom : struct, IColorProfile<TFrom, CieXyz>
where TTo : struct, IColorProfile<TTo, Rgb>

124
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs

@ -4,10 +4,13 @@
using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Numerics.Tensors;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.ColorProfiles.Conversion.Icc;
using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
@ -39,6 +42,24 @@ internal static class ColorProfileConverterExtensionsIcc
0.0033717495F, 0.0034852044F, 0.0028800198F, 0F,
0.0033717495F, 0.0034852044F, 0.0028800198F, 0F];
/// <summary>
/// Converts a color value from one ICC color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// This method performs color conversion using ICC profiles, ensuring accurate color mapping
/// between different color spaces. Both the source and target ICC profiles must be provided in the converter's
/// options. The method supports perceptual adjustments when required by the profiles.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo}"/>.</typeparam>
/// <param name="converter">The color profile converter configured with source and target ICC profiles.</param>
/// <param name="source">The color value to convert, defined in the source color profile.</param>
/// <returns>
/// A color value in the target color profile, resulting from the ICC profile-based conversion of the source value.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Thrown if either the source or target ICC profile is missing from the converter options.
/// </exception>
internal static TTo ConvertUsingIccProfile<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
where TFrom : struct, IColorProfile<TFrom>
where TTo : struct, IColorProfile<TTo>
@ -60,8 +81,8 @@ internal static class ColorProfileConverterExtensionsIcc
ColorProfileConverter pcsConverter = new(new ColorConversionOptions
{
MemoryAllocator = converter.Options.MemoryAllocator,
SourceWhitePoint = new CieXyz(converter.Options.SourceIccProfile.Header.PcsIlluminant),
TargetWhitePoint = new CieXyz(converter.Options.TargetIccProfile.Header.PcsIlluminant),
SourceWhitePoint = KnownIlluminants.D50Icc,
TargetWhitePoint = KnownIlluminants.D50Icc
});
// Normalize the source, then convert to the PCS space.
@ -81,6 +102,29 @@ internal static class ColorProfileConverterExtensionsIcc
return TTo.FromScaledVector4(targetParams.Converter.Calculate(targetPcs));
}
/// <summary>
/// Converts a span of color values from a source color profile to a destination color profile using ICC profiles.
/// </summary>
/// <remarks>
/// This method performs color conversion by transforming the input values through the Profile
/// Connection Space (PCS) as defined by the provided ICC profiles. Perceptual adjustments are applied as required
/// by the profiles. The method does not support absolute colorimetric intent and will not perform such
/// conversions.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo}"/>.</typeparam>
/// <param name="converter">The color profile converter that provides conversion options and ICC profiles.</param>
/// <param name="source">
/// A read-only span containing the source color values to convert. The values must conform to the source color
/// profile.
/// </param>
/// <param name="destination">
/// A span to receive the converted color values in the destination color profile. Must be at least as large as the
/// source span.
/// </param>
/// <exception cref="InvalidOperationException">
/// Thrown if the source or target ICC profile is missing from the converter options.
/// </exception>
internal static void ConvertUsingIccProfile<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
where TFrom : struct, IColorProfile<TFrom>
where TTo : struct, IColorProfile<TTo>
@ -104,8 +148,8 @@ internal static class ColorProfileConverterExtensionsIcc
ColorProfileConverter pcsConverter = new(new ColorConversionOptions
{
MemoryAllocator = converter.Options.MemoryAllocator,
SourceWhitePoint = new CieXyz(converter.Options.SourceIccProfile.Header.PcsIlluminant),
TargetWhitePoint = new CieXyz(converter.Options.TargetIccProfile.Header.PcsIlluminant),
SourceWhitePoint = KnownIlluminants.D50Icc,
TargetWhitePoint = KnownIlluminants.D50Icc
});
using IMemoryOwner<Vector4> pcsBuffer = converter.Options.MemoryAllocator.Allocate<Vector4>(source.Length);
@ -617,38 +661,10 @@ internal static class ColorProfileConverterExtensionsIcc
private static void ClipNegative(Span<Vector4> source)
{
if (Vector.IsHardwareAccelerated && Vector<float>.IsSupported && Vector<float>.Count >= source.Length * 4)
{
// SIMD loop
int i = 0;
int simdBatchSize = Vector<float>.Count / 4; // Number of Vector4 elements per SIMD batch
for (; i <= source.Length - simdBatchSize; i += simdBatchSize)
{
// Load the vector from source span
Vector<float> v = Unsafe.ReadUnaligned<Vector<float>>(ref Unsafe.As<Vector4, byte>(ref source[i]));
v = Vector.Max(v, Vector<float>.Zero);
// Write the vector to the destination span
Unsafe.WriteUnaligned(ref Unsafe.As<Vector4, byte>(ref source[i]), v);
}
// Scalar fallback for remaining elements
for (; i < source.Length; i++)
{
ref Vector4 s = ref source[i];
s = Vector4.Max(s, Vector4.Zero);
}
}
else
{
// Scalar fallback if SIMD is not supported
for (int i = 0; i < source.Length; i++)
{
ref Vector4 s = ref source[i];
s = Vector4.Max(s, Vector4.Zero);
}
}
// Vector4 values are contiguous floats, so flattening preserves the component order
// while allowing one shared tensor traversal to process every channel and SIMD tail.
Span<float> values = MemoryMarshal.Cast<Vector4, float>(source);
TensorPrimitives.Max(values, 0F, values);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -667,39 +683,9 @@ internal static class ColorProfileConverterExtensionsIcc
private static void LabToLab(Span<Vector4> source, Span<Vector4> destination, [ConstantExpected] float scale)
{
if (Vector.IsHardwareAccelerated && Vector<float>.IsSupported)
{
Vector<float> vScale = new(scale);
int i = 0;
// SIMD loop
int simdBatchSize = Vector<float>.Count / 4; // Number of Vector4 elements per SIMD batch
for (; i <= source.Length - simdBatchSize; i += simdBatchSize)
{
// Load the vector from source span
Vector<float> v = Unsafe.ReadUnaligned<Vector<float>>(ref Unsafe.As<Vector4, byte>(ref source[i]));
// Scale the vector
v *= vScale;
// Write the scaled vector to the destination span
Unsafe.WriteUnaligned(ref Unsafe.As<Vector4, byte>(ref destination[i]), v);
}
// Scalar fallback for remaining elements
for (; i < source.Length; i++)
{
destination[i] = source[i] * scale;
}
}
else
{
// Scalar fallback if SIMD is not supported
for (int i = 0; i < source.Length; i++)
{
destination[i] = source[i] * scale;
}
}
// Reinterpreting both spans exposes all four components to one multiplication traversal;
// the source and destination retain their original Vector4 boundaries after the operation.
TensorPrimitives.Multiply(MemoryMarshal.Cast<Vector4, float>(source), scale, MemoryMarshal.Cast<Vector4, float>(destination));
}
private class ConversionParams

192
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs

@ -0,0 +1,192 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsPixelCompatible
{
/// <summary>
/// Converts the pixel data of the specified image from the source color profile to the target color profile using
/// the provided color profile converter.
/// </summary>
/// <remarks>
/// This method modifies the source image in place by converting its pixel data according to the
/// color profiles specified in the converter. The method does not verify whether the profiles are RGB compatible;
/// if they are not, the conversion may produce incorrect results. Ensure that both the source and target ICC
/// profiles are set on the converter before calling this method.
/// </remarks>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="converter">The color profile converter configured with source and target ICC profiles.</param>
/// <param name="source">
/// The image whose pixel data will be converted. The conversion is performed in place, modifying the original
/// image.
/// </param>
/// <exception cref="InvalidOperationException">
/// Thrown if the converter's source or target ICC profile is not specified.
/// </exception>
public static void Convert<TPixel>(this ColorProfileConverter converter, Image<TPixel> source)
where TPixel : unmanaged, IPixel<TPixel>
{
// These checks actually take place within the converter, but we want to fail fast here.
// Note. we do not check to see whether the profiles themselves are RGB compatible,
// if they are not, then the converter will simply produce incorrect results.
if (converter.Options.SourceIccProfile is null)
{
throw new InvalidOperationException("Source ICC profile is missing.");
}
if (converter.Options.TargetIccProfile is null)
{
throw new InvalidOperationException("Target ICC profile is missing.");
}
// Process the rows in parallel chunks, the converter itself is thread safe.
source.Mutate(o => o.ProcessPixelRowsAsVector4(
row =>
{
// Gather and convert the pixels in the row to Rgb.
using IMemoryOwner<Rgb> rgbBuffer = converter.Options.MemoryAllocator.Allocate<Rgb>(row.Length);
Span<Rgb> rgbSpan = rgbBuffer.Memory.Span;
Rgb.FromScaledVector4(row, rgbSpan);
// Perform the actual color conversion.
converter.ConvertUsingIccProfile<Rgb, Rgb>(rgbSpan, rgbSpan);
// Copy the converted Rgb pixels back to the row as TPixel.
// Important: Preserve alpha from the existing row Vector4 values.
// We merge RGB from rgbSpan into row, leaving W untouched.
ref float srcRgb = ref Unsafe.As<Rgb, float>(ref MemoryMarshal.GetReference(rgbSpan));
ref float dstRow = ref Unsafe.As<Vector4, float>(ref MemoryMarshal.GetReference(row));
int count = rgbSpan.Length;
int i = 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static Vector512<float> ReadVector512(ref float f)
{
ref byte b = ref Unsafe.As<float, byte>(ref f);
return Unsafe.ReadUnaligned<Vector512<float>>(ref b);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void WriteVector512(ref float f, Vector512<float> v)
{
ref byte b = ref Unsafe.As<float, byte>(ref f);
Unsafe.WriteUnaligned(ref b, v);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static Vector256<float> ReadVector256(ref float f)
{
ref byte b = ref Unsafe.As<float, byte>(ref f);
return Unsafe.ReadUnaligned<Vector256<float>>(ref b);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void WriteVector256(ref float f, Vector256<float> v)
{
ref byte b = ref Unsafe.As<float, byte>(ref f);
Unsafe.WriteUnaligned(ref b, v);
}
if (Avx512F.IsSupported)
{
// 4 pixels per iteration.
//
// Source layout (Rgb float stream, 12 floats):
// [r0 g0 b0 r1 g1 b1 r2 g2 b2 r3 g3 b3]
//
// Destination layout (row Vector4 float stream, 16 floats):
// [r0 g0 b0 a0 r1 g1 b1 a1 r2 g2 b2 a2 r3 g3 b3 a3]
//
// We use an overlapped load (16 floats) from the 3-float stride source.
// The permute selects the RGB we need and inserts placeholders for alpha lanes.
//
// Then we blend RGB lanes into the existing destination, preserving alpha lanes.
Vector512<int> rgbPerm = Vector512.Create(0, 1, 2, 0, 3, 4, 5, 0, 6, 7, 8, 0, 9, 10, 11, 0);
// BlendVariable selects from the second operand where the sign bit of the mask lane is set.
// We want to overwrite lanes 0,1,2 then 4,5,6 then 8,9,10 then 12,13,14, and preserve lanes 3,7,11,15 (alpha).
Vector512<float> rgbSelect = Vector512.Create(-0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F);
int quads = count >> 2;
int simdQuads = quads - 1; // Leave the last quad for the scalar tail to avoid the final overlapped load reading past the end.
for (int q = 0; q < simdQuads; q++)
{
Vector512<float> dst = ReadVector512(ref dstRow);
Vector512<float> src = ReadVector512(ref srcRgb);
Vector512<float> rgbx = Avx512F.PermuteVar16x32(src, rgbPerm);
Vector512<float> merged = Avx512F.BlendVariable(dst, rgbx, rgbSelect);
WriteVector512(ref dstRow, merged);
// Advance input by 4 pixels (4 * 3 = 12 floats)
srcRgb = ref Unsafe.Add(ref srcRgb, 12);
// Advance output by 4 pixels (4 * 4 = 16 floats)
dstRow = ref Unsafe.Add(ref dstRow, 16);
i += 4;
}
}
else if (Avx2.IsSupported)
{
// 2 pixels per iteration.
//
// Same idea as AVX-512, but on 256-bit vectors.
// We permute packed RGB into rgbx layout and blend into the existing destination,
// preserving alpha lanes.
Vector256<int> rgbPerm = Vector256.Create(0, 1, 2, 0, 3, 4, 5, 0);
Vector256<float> rgbSelect = Vector256.Create(-0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F);
int pairs = count >> 1;
int simdPairs = pairs - 1; // Leave the last pair for the scalar tail to avoid the final overlapped load reading past the end.
for (int p = 0; p < simdPairs; p++)
{
Vector256<float> dst = ReadVector256(ref dstRow);
Vector256<float> src = ReadVector256(ref srcRgb);
Vector256<float> rgbx = Avx2.PermuteVar8x32(src, rgbPerm);
Vector256<float> merged = Avx.BlendVariable(dst, rgbx, rgbSelect);
WriteVector256(ref dstRow, merged);
// Advance input by 2 pixels (2 * 3 = 6 floats)
srcRgb = ref Unsafe.Add(ref srcRgb, 6);
// Advance output by 2 pixels (2 * 4 = 8 floats)
dstRow = ref Unsafe.Add(ref dstRow, 8);
i += 2;
}
}
// Scalar tail.
// Handles:
// - the last skipped SIMD block (quad or pair)
// - any remainder
//
// Preserve alpha by writing Vector3 into the Vector4 storage.
ref Vector4 rowRef = ref MemoryMarshal.GetReference(row);
for (; i < count; i++)
{
Vector3 rgb = rgbSpan[i].AsVector3Unsafe();
Unsafe.As<Vector4, Vector3>(ref Unsafe.Add(ref rowRef, (uint)i)) = rgb;
}
},
PixelConversionModifiers.Scale));
}
}

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs

@ -6,8 +6,24 @@ using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsRgbCieLab
/// <summary>
/// Allows conversion between two color profiles based on the RGB and CIE Lab color spaces.
/// </summary>
public static class ColorProfileConverterExtensionsRgbCieLab
{
/// <summary>
/// Converts a color value from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// The conversion process may use ICC profiles if available; otherwise, it performs a manual
/// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires
/// both source and target types to be value types implementing the appropriate color profile interface.
/// </remarks>
/// <typeparam name="TFrom">The source color profile type. Must implement <see cref="IColorProfile{TFrom, Rgb}"/>.</typeparam>
/// <typeparam name="TTo">The target color profile type. Must implement <see cref="IColorProfile{TTo, CieLab}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion.</param>
/// <param name="source">The source color value to convert.</param>
/// <returns>A value of type <typeparamref name="TTo"/> representing the converted color in the target color profile.</returns>
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
where TFrom : struct, IColorProfile<TFrom, Rgb>
where TTo : struct, IColorProfile<TTo, CieLab>
@ -34,6 +50,20 @@ internal static class ColorProfileConverterExtensionsRgbCieLab
return TTo.FromProfileConnectingSpace(options, in pcsTo);
}
/// <summary>
/// Converts a span of color values from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// This method performs color conversion between two color profiles, handling necessary
/// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are
/// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory
/// for the destination; the caller is responsible for providing a suitably sized span.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom, Rgb}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo, CieLab}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion operation.</param>
/// <param name="source">A read-only span containing the source color values to convert.</param>
/// <param name="destination">A span that receives the converted color values. Must be at least as long as the source span.</param>
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
where TFrom : struct, IColorProfile<TFrom, Rgb>
where TTo : struct, IColorProfile<TTo, CieLab>

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs

@ -6,8 +6,24 @@ using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsRgbCieXyz
/// <summary>
/// Allows conversion between two color profiles based on the RGB and CIE XYZ color spaces.
/// </summary>
public static class ColorProfileConverterExtensionsRgbCieXyz
{
/// <summary>
/// Converts a color value from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// The conversion process may use ICC profiles if available; otherwise, it performs a manual
/// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires
/// both source and target types to be value types implementing the appropriate color profile interface.
/// </remarks>
/// <typeparam name="TFrom">The source color profile type. Must implement <see cref="IColorProfile{TFrom, Rgb}"/>.</typeparam>
/// <typeparam name="TTo">The target color profile type. Must implement <see cref="IColorProfile{TTo, CieXyz}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion.</param>
/// <param name="source">The source color value to convert.</param>
/// <returns>A value of type <typeparamref name="TTo"/> representing the converted color in the target color profile.</returns>
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
where TFrom : struct, IColorProfile<TFrom, Rgb>
where TTo : struct, IColorProfile<TTo, CieXyz>
@ -33,6 +49,20 @@ internal static class ColorProfileConverterExtensionsRgbCieXyz
return TTo.FromProfileConnectingSpace(options, in pcsTo);
}
/// <summary>
/// Converts a span of color values from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// This method performs color conversion between two color profiles, handling necessary
/// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are
/// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory
/// for the destination; the caller is responsible for providing a suitably sized span.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom, Rgb}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo, CieXyz}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion operation.</param>
/// <param name="source">A read-only span containing the source color values to convert.</param>
/// <param name="destination">A span that receives the converted color values. Must be at least as long as the source span.</param>
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
where TFrom : struct, IColorProfile<TFrom, Rgb>
where TTo : struct, IColorProfile<TTo, CieXyz>

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs

@ -6,8 +6,24 @@ using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.ColorProfiles;
internal static class ColorProfileConverterExtensionsRgbRgb
/// <summary>
/// Allows conversion between two color profiles based on the RGB color space.
/// </summary>
public static class ColorProfileConverterExtensionsRgbRgb
{
/// <summary>
/// Converts a color value from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// The conversion process may use ICC profiles if available; otherwise, it performs a manual
/// conversion through the profile connection space (PCS) with chromatic adaptation as needed. The method requires
/// both source and target types to be value types implementing the appropriate color profile interface.
/// </remarks>
/// <typeparam name="TFrom">The source color profile type. Must implement <see cref="IColorProfile{TFrom, Rgb}"/>.</typeparam>
/// <typeparam name="TTo">The target color profile type. Must implement <see cref="IColorProfile{TTo, Rgb}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion.</param>
/// <param name="source">The source color value to convert.</param>
/// <returns>A value of type <typeparamref name="TTo"/> representing the converted color in the target color profile.</returns>
public static TTo Convert<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
where TFrom : struct, IColorProfile<TFrom, Rgb>
where TTo : struct, IColorProfile<TTo, Rgb>
@ -34,6 +50,20 @@ internal static class ColorProfileConverterExtensionsRgbRgb
return TTo.FromProfileConnectingSpace(options, in pcsTo);
}
/// <summary>
/// Converts a span of color values from one color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// This method performs color conversion between two color profiles, handling necessary
/// transformations such as profile connection space conversion and chromatic adaptation. If ICC profiles are
/// available and applicable, the conversion uses them for improved accuracy. The method does not allocate memory
/// for the destination; the caller is responsible for providing a suitably sized span.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom, Rgb}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo, Rgb}"/>.</typeparam>
/// <param name="converter">The color profile converter to use for the conversion operation.</param>
/// <param name="source">A read-only span containing the source color values to convert.</param>
/// <param name="destination">A span that receives the converted color values. Must be at least as long as the source span.</param>
public static void Convert<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
where TFrom : struct, IColorProfile<TFrom, Rgb>
where TTo : struct, IColorProfile<TTo, Rgb>

2
src/ImageSharp/ColorProfiles/Icc/Calculators/GrayTrcCalculator.cs

@ -12,7 +12,7 @@ internal class GrayTrcCalculator : IVector4Calculator
private readonly TrcCalculator calculator;
public GrayTrcCalculator(IccTagDataEntry grayTrc, bool toPcs)
=> this.calculator = new TrcCalculator(new IccTagDataEntry[] { grayTrc }, !toPcs);
=> this.calculator = new TrcCalculator([grayTrc], !toPcs);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 Calculate(Vector4 value) => this.calculator.Calculate(value);

17
src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs

@ -5,14 +5,19 @@ namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc;
internal partial class LutABCalculator
{
/// <summary>
/// Identifies the transform direction for the configured LUT calculator.
/// </summary>
private enum CalculationType
{
AtoB = 1 << 3,
BtoA = 1 << 4,
/// <summary>
/// Converts from device space to PCS using ICC <c>mAB</c> stage order.
/// </summary>
AtoB,
SingleCurve = 1,
CurveMatrix = 2,
CurveClut = 3,
Full = 4,
/// <summary>
/// Converts from PCS to device space using ICC <c>mBA</c> stage order.
/// </summary>
BtoA,
}
}

141
src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs

@ -17,67 +17,106 @@ internal partial class LutABCalculator : IVector4Calculator
private MatrixCalculator matrixCalculator;
private ClutCalculator clutCalculator;
/// <summary>
/// Initializes a new instance of the <see cref="LutABCalculator"/> class for an ICC <c>mAB</c> transform.
/// </summary>
/// <param name="entry">The parsed A-to-B LUT entry.</param>
public LutABCalculator(IccLutAToBTagDataEntry entry)
{
Guard.NotNull(entry, nameof(entry));
this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues);
this.type |= CalculationType.AtoB;
this.type = CalculationType.AtoB;
}
/// <summary>
/// Initializes a new instance of the <see cref="LutABCalculator"/> class for an ICC <c>mBA</c> transform.
/// </summary>
/// <param name="entry">The parsed B-to-A LUT entry.</param>
public LutABCalculator(IccLutBToATagDataEntry entry)
{
Guard.NotNull(entry, nameof(entry));
this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues);
this.type |= CalculationType.BtoA;
this.type = CalculationType.BtoA;
}
/// <summary>
/// Calculates the transformed value by applying the configured ICC LUT stages in specification order.
/// </summary>
/// <param name="value">The input value.</param>
/// <returns>The transformed value.</returns>
public Vector4 Calculate(Vector4 value)
{
switch (this.type)
{
case CalculationType.Full | CalculationType.AtoB:
value = this.curveACalculator.Calculate(value);
value = this.clutCalculator.Calculate(value);
value = this.curveMCalculator.Calculate(value);
value = this.matrixCalculator.Calculate(value);
return this.curveBCalculator.Calculate(value);
case CalculationType.Full | CalculationType.BtoA:
value = this.curveBCalculator.Calculate(value);
value = this.matrixCalculator.Calculate(value);
value = this.curveMCalculator.Calculate(value);
value = this.clutCalculator.Calculate(value);
return this.curveACalculator.Calculate(value);
case CalculationType.CurveClut | CalculationType.AtoB:
value = this.curveACalculator.Calculate(value);
value = this.clutCalculator.Calculate(value);
return this.curveBCalculator.Calculate(value);
case CalculationType.CurveClut | CalculationType.BtoA:
value = this.curveBCalculator.Calculate(value);
value = this.clutCalculator.Calculate(value);
return this.curveACalculator.Calculate(value);
case CalculationType.CurveMatrix | CalculationType.AtoB:
value = this.curveMCalculator.Calculate(value);
value = this.matrixCalculator.Calculate(value);
return this.curveBCalculator.Calculate(value);
case CalculationType.CurveMatrix | CalculationType.BtoA:
value = this.curveBCalculator.Calculate(value);
value = this.matrixCalculator.Calculate(value);
return this.curveMCalculator.Calculate(value);
case CalculationType.SingleCurve | CalculationType.AtoB:
case CalculationType.SingleCurve | CalculationType.BtoA:
return this.curveBCalculator.Calculate(value);
case CalculationType.AtoB:
// ICC mAB order: A, CLUT, M, Matrix, B.
if (this.curveACalculator != null)
{
value = this.curveACalculator.Calculate(value);
}
if (this.clutCalculator != null)
{
value = this.clutCalculator.Calculate(value);
}
if (this.curveMCalculator != null)
{
value = this.curveMCalculator.Calculate(value);
}
if (this.matrixCalculator != null)
{
value = this.matrixCalculator.Calculate(value);
}
if (this.curveBCalculator != null)
{
value = this.curveBCalculator.Calculate(value);
}
return value;
case CalculationType.BtoA:
// ICC mBA order: B, Matrix, M, CLUT, A.
if (this.curveBCalculator != null)
{
value = this.curveBCalculator.Calculate(value);
}
if (this.matrixCalculator != null)
{
value = this.matrixCalculator.Calculate(value);
}
if (this.curveMCalculator != null)
{
value = this.curveMCalculator.Calculate(value);
}
if (this.clutCalculator != null)
{
value = this.clutCalculator.Calculate(value);
}
if (this.curveACalculator != null)
{
value = this.curveACalculator.Calculate(value);
}
return value;
default:
throw new InvalidOperationException("Invalid calculation type");
}
}
/// <summary>
/// Creates calculators for the processing stages present in the LUT entry.
/// </summary>
/// <remarks>
/// The tag entry classes already validate channel continuity, so this method only materializes the available stages.
/// </remarks>
private void Init(IccTagDataEntry[] curveA, IccTagDataEntry[] curveB, IccTagDataEntry[] curveM, Vector3? matrix3x1, Matrix4x4? matrix3x3, IccClut clut)
{
bool hasACurve = curveA != null;
@ -86,26 +125,10 @@ internal partial class LutABCalculator : IVector4Calculator
bool hasMatrix = matrix3x1 != null && matrix3x3 != null;
bool hasClut = clut != null;
if (hasBCurve && hasMatrix && hasMCurve && hasClut && hasACurve)
{
this.type = CalculationType.Full;
}
else if (hasBCurve && hasClut && hasACurve)
{
this.type = CalculationType.CurveClut;
}
else if (hasBCurve && hasMatrix && hasMCurve)
{
this.type = CalculationType.CurveMatrix;
}
else if (hasBCurve)
{
this.type = CalculationType.SingleCurve;
}
else
{
throw new InvalidIccProfileException("AToB or BToA tag has an invalid configuration");
}
Guard.IsTrue(
hasACurve || hasBCurve || hasMCurve || hasMatrix || hasClut,
"entry",
"AToB or BToA tag must contain at least one processing element");
if (hasACurve)
{

2
src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs

@ -60,7 +60,7 @@ internal abstract partial class IccConverterBase
IccLut16TagDataEntry lut16 => new LutEntryCalculator(lut16),
IccLutAToBTagDataEntry lutAtoB => new LutABCalculator(lutAtoB),
IccLutBToATagDataEntry lutBtoA => new LutABCalculator(lutBtoA),
_ => throw new InvalidIccProfileException("Invalid entry."),
_ => throw new InvalidIccProfileException($"Invalid entry {tag}."),
};
private static IVector4Calculator InitD(IccProfile profile, IccProfileTag tag)

8
src/ImageSharp/ColorProfiles/KnownIlluminants.cs

@ -9,6 +9,7 @@ namespace SixLabors.ImageSharp.ColorProfiles;
/// </summary>
/// <remarks>
/// Coefficients taken from: http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
/// and https://color.org/specification/ICC.1-2022-05.pdf
/// <br />
/// Descriptions taken from: http://en.wikipedia.org/wiki/Standard_illuminant
/// </remarks>
@ -30,10 +31,15 @@ public static class KnownIlluminants
public static CieXyz C { get; } = new(0.98074F, 1F, 1.18232F);
/// <summary>
/// Gets the Horizon Light. ICC profile PCS illuminant.
/// Gets the Horizon Light.
/// </summary>
public static CieXyz D50 { get; } = new(0.96422F, 1F, 0.82521F);
/// <summary>
/// Gets the D50 illuminant used in the ICC profile specification.
/// </summary>
public static CieXyz D50Icc { get; } = new(0.9642F, 1F, 0.8249F);
/// <summary>
/// Gets the Mid-morning / Mid-afternoon Daylight illuminant.
/// </summary>

184
src/ImageSharp/ColorProfiles/Rgb.cs

@ -4,6 +4,8 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces;
namespace SixLabors.ImageSharp.ColorProfiles;
@ -105,10 +107,87 @@ public readonly struct Rgb : IProfileConnectingSpace<Rgb, CieXyz>
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
// TODO: Optimize via SIMD
for (int i = 0; i < source.Length; i++)
int length = source.Length;
if (length == 0)
{
destination[i] = source[i].ToScaledVector4();
return;
}
ref Rgb srcRgb = ref MemoryMarshal.GetReference(source);
ref Vector4 dstV4 = ref MemoryMarshal.GetReference(destination);
// Float streams:
// src: r0 g0 b0 r1 g1 b1 ...
// dst: r0 g0 b0 a0 r1 g1 b1 a1 ...
ref float src = ref Unsafe.As<Rgb, float>(ref srcRgb);
ref float dst = ref Unsafe.As<Vector4, float>(ref dstV4);
int i = 0;
if (Avx512F.IsSupported)
{
// 4 pixels per iteration. Using overlapped 16-float loads.
Vector512<int> perm = Vector512.Create(0, 1, 2, 0, 3, 4, 5, 0, 6, 7, 8, 0, 9, 10, 11, 0);
Vector512<float> ones = Vector512.Create(1F);
// BlendVariable selects from 'ones' where the sign-bit of mask lane is set.
// Using -0f sets only the sign bit, producing an efficient "select lane" mask.
Vector512<float> alphaSelect = Vector512.Create(0F, 0F, 0F, -0F, 0F, 0F, 0F, -0F, 0F, 0F, 0F, -0F, 0F, 0F, 0F, -0F);
int quads = length >> 2;
// Leave the last quad (4 pixels) for the scalar tail.
int simdQuads = quads - 1;
for (int q = 0; q < simdQuads; q++)
{
Vector512<float> v = ReadVector512(ref src);
Vector512<float> rgbx = Avx512F.PermuteVar16x32(v, perm);
Vector512<float> rgba = Avx512F.BlendVariable(rgbx, ones, alphaSelect);
WriteVector512(ref dst, rgba);
src = ref Unsafe.Add(ref src, 12);
dst = ref Unsafe.Add(ref dst, 16);
i += 4;
}
}
else if (Avx2.IsSupported)
{
// 2 pixels per iteration. Using overlapped 8-float loads.
Vector256<int> perm = Vector256.Create(0, 1, 2, 0, 3, 4, 5, 0);
Vector256<float> ones = Vector256.Create(1F);
// vblendps mask: bit i selects lane i from 'ones' when set.
// We want lanes 3 and 7 -> 0b10001000 = 0x88.
const byte alphaMask = 0x88;
int pairs = length >> 1;
// Leave the last pair (2 pixels) for the scalar tail.
int simdPairs = pairs - 1;
for (int p = 0; p < simdPairs; p++)
{
Vector256<float> v = ReadVector256(ref src);
Vector256<float> rgbx = Avx2.PermuteVar8x32(v, perm);
Vector256<float> rgba = Avx.Blend(rgbx, ones, alphaMask);
WriteVector256(ref dst, rgba);
src = ref Unsafe.Add(ref src, 6);
dst = ref Unsafe.Add(ref dst, 8);
i += 2;
}
}
// Tail (and non-AVX paths)
for (; i < length; i++)
{
Unsafe.Add(ref dstV4, i) = Unsafe.Add(ref srcRgb, i).ToScaledVector4();
}
}
@ -117,10 +196,75 @@ public readonly struct Rgb : IProfileConnectingSpace<Rgb, CieXyz>
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
// TODO: Optimize via SIMD
for (int i = 0; i < source.Length; i++)
int length = source.Length;
if (length == 0)
{
destination[i] = FromScaledVector4(source[i]);
return;
}
ref Vector4 srcV4 = ref MemoryMarshal.GetReference(source);
ref Rgb dstRgb = ref MemoryMarshal.GetReference(destination);
// Float streams:
// src: r0 g0 b0 a0 r1 g1 b1 a1 ...
// dst: r0 g0 b0 r1 g1 b1 ...
ref float src = ref Unsafe.As<Vector4, float>(ref srcV4);
ref float dst = ref Unsafe.As<Rgb, float>(ref dstRgb);
int i = 0;
if (Avx512F.IsSupported)
{
// 4 pixels per iteration. Using overlapped 16-float stores:
Vector512<int> idx = Vector512.Create(0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 3, 7, 11, 15);
// Number of 4-pixel groups in the input.
int quads = length >> 2;
// Leave the last quad (4 pixels) for the scalar tail.
int simdQuads = quads - 1;
for (int q = 0; q < simdQuads; q++)
{
Vector512<float> v = ReadVector512(ref src);
Vector512<float> packed = Avx512F.PermuteVar16x32(v, idx);
WriteVector512(ref dst, packed);
src = ref Unsafe.Add(ref src, 16);
dst = ref Unsafe.Add(ref dst, 12);
i += 4;
}
}
else if (Avx2.IsSupported)
{
// 2 pixels per iteration, using overlapped 8-float stores:
Vector256<int> idx = Vector256.Create(0, 1, 2, 4, 5, 6, 0, 0);
int pairs = length >> 1;
// Leave the last pair (2 pixels) for the scalar tail.
int simdPairs = pairs - 1;
int pairIndex = 0;
for (; pairIndex < simdPairs; pairIndex++)
{
Vector256<float> v = ReadVector256(ref src);
Vector256<float> packed = Avx2.PermuteVar8x32(v, idx);
WriteVector256(ref dst, packed);
src = ref Unsafe.Add(ref src, 8);
dst = ref Unsafe.Add(ref dst, 6);
i += 2;
}
}
// Tail (and non-AVX paths)
for (; i < length; i++)
{
Vector4 v = Unsafe.Add(ref srcV4, i);
Unsafe.Add(ref dstRgb, i) = FromScaledVector4(v);
}
}
@ -288,4 +432,32 @@ public readonly struct Rgb : IProfileConnectingSpace<Rgb, CieXyz>
M44 = 1F
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> ReadVector512(ref float src)
{
ref byte b = ref Unsafe.As<float, byte>(ref src);
return Unsafe.ReadUnaligned<Vector512<float>>(ref b);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> ReadVector256(ref float src)
{
ref byte b = ref Unsafe.As<float, byte>(ref src);
return Unsafe.ReadUnaligned<Vector256<float>>(ref b);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteVector512(ref float dst, Vector512<float> value)
{
ref byte b = ref Unsafe.As<float, byte>(ref dst);
Unsafe.WriteUnaligned(ref b, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteVector256(ref float dst, Vector256<float> value)
{
ref byte b = ref Unsafe.As<float, byte>(ref dst);
Unsafe.WriteUnaligned(ref b, value);
}
}

1
src/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs

@ -62,6 +62,7 @@ public sealed class GammaWorkingSpace : RgbWorkingSpace
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(
typeof(GammaWorkingSpace),
this.WhitePoint,
this.ChromaticityCoordinates,
this.Gamma);

6
src/ImageSharp/ColorProfiles/WorkingSpaces/RgbWorkingSpace.cs

@ -70,8 +70,10 @@ public abstract class RgbWorkingSpace
return true;
}
if (obj is RgbWorkingSpace other)
if (obj.GetType() == this.GetType())
{
RgbWorkingSpace other = (RgbWorkingSpace)obj;
return this.WhitePoint.Equals(other.WhitePoint)
&& this.ChromaticityCoordinates.Equals(other.ChromaticityCoordinates);
}
@ -81,5 +83,5 @@ public abstract class RgbWorkingSpace
/// <inheritdoc/>
public override int GetHashCode()
=> HashCode.Combine(this.WhitePoint, this.ChromaticityCoordinates);
=> HashCode.Combine(this.GetType(), this.WhitePoint, this.ChromaticityCoordinates);
}

4
src/ImageSharp/Common/Extensions/StreamExtensions.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
@ -17,7 +17,7 @@ internal static class StreamExtensions
/// <param name="buffer">The buffer.</param>
/// <param name="offset">The offset within the buffer to begin writing.</param>
/// <param name="count">The number of bytes to write to the stream.</param>
public static void Write(this Stream stream, Span<byte> buffer, int offset, int count)
public static void Write(this Stream stream, ReadOnlySpan<byte> buffer, int offset, int count)
=> stream.Write(buffer.Slice(offset, count));
/// <summary>

67
src/ImageSharp/Common/Helpers/ColorNumerics.cs

@ -26,7 +26,7 @@ internal static class ColorNumerics
/// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images).
/// </param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetBT709Luminance(ref Vector4 vector, int luminanceLevels)
public static int GetBT709Luminance(Vector4 vector, int luminanceLevels)
=> (int)MathF.Round(Vector4.Dot(vector, Bt709) * (luminanceLevels - 1));
/// <summary>
@ -97,7 +97,7 @@ internal static class ColorNumerics
/// Scales a value from a 16 bit <see cref="ushort"/> to an
/// 8 bit <see cref="byte"/> equivalent.
/// </summary>
/// <param name="component">The 8 bit component value.</param>
/// <param name="component">The 16 bit component value.</param>
/// <returns>The <see cref="byte"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte From16BitTo8Bit(ushort component) =>
@ -132,6 +132,49 @@ internal static class ColorNumerics
// (V * 255 + 32895) >> 16
(byte)(((component * 255) + 32895) >> 16);
/// <summary>
/// Scales a value from a 32 bit <see cref="uint"/> to an
/// 8 bit <see cref="byte"/> equivalent.
/// </summary>
/// <param name="component">The 32 bit component value.</param>
/// <returns>The <see cref="byte"/> value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte From32BitTo8Bit(uint component) =>
// To scale to 8 bits from a 32-bit value V the required value is:
//
// (V * 255) / 4294967295
//
// Since:
//
// 4294967295 = 255 * 16843009
//
// this reduces exactly to:
//
// V / 16843009
//
// To round to nearest using integer arithmetic we add half the divisor
// before dividing:
//
// (V + 16843009 / 2) / 16843009
//
// where:
//
// 16843009 / 2 = 8421504.5
//
// Using 8421504 ensures correct round-to-nearest behaviour:
//
// 8421504 -> 0
// 8421505 -> 1
//
// The addition must be performed in 64-bit to avoid overflow for large
// input values (for example uint.MaxValue).
//
// Final exact integer implementation:
//
// ((ulong)V + 8421504) / 16843009
(byte)((component + 8421504UL) / 16843009UL);
/// <summary>
/// Scales a value from an 8 bit <see cref="byte"/> to
/// an 16 bit <see cref="ushort"/> equivalent.
@ -142,6 +185,26 @@ internal static class ColorNumerics
public static ushort From8BitTo16Bit(byte component)
=> (ushort)(component * 257);
/// <summary>
/// Scales a value from an 16 bit <see cref="byte"/> to
/// an 16 bit <see cref="uint"/> equivalent.
/// </summary>
/// <param name="component">The 16 bit component value.</param>
/// <returns>The 32 bit <see cref="uint"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint From16BitTo32Bit(ushort component)
=> (uint)(component * 65537);
/// <summary>
/// Scales a value from an 8 bit <see cref="byte"/> to
/// an 32 bit <see cref="ushort"/> equivalent.
/// </summary>
/// <param name="component">The 8 bit component value.</param>
/// <returns>The 32 bit <see cref="uint"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint From8BitTo32Bit(byte component)
=> (uint)(component * 16843009);
/// <summary>
/// Returns how many bits are required to store the specified number of colors.
/// Performs a Log2() on the value.

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

@ -2,10 +2,12 @@
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Numerics.Tensors;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp;
@ -328,22 +330,7 @@ internal static class Numerics
/// <param name="max">The maximum inclusive value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(Span<byte> span, byte min, byte max)
{
Span<byte> remainder = span[ClampReduce(span, min, max)..];
if (remainder.Length > 0)
{
ref byte remainderStart = ref MemoryMarshal.GetReference(remainder);
ref byte remainderEnd = ref Unsafe.Add(ref remainderStart, (uint)remainder.Length);
while (Unsafe.IsAddressLessThan(ref remainderStart, ref remainderEnd))
{
remainderStart = Clamp(remainderStart, min, max);
remainderStart = ref Unsafe.Add(ref remainderStart, 1);
}
}
}
=> TensorPrimitives.Clamp(span, min, max, span);
/// <summary>
/// Clamps the span values to the inclusive range of min and max.
@ -353,22 +340,7 @@ internal static class Numerics
/// <param name="max">The maximum inclusive value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(Span<uint> span, uint min, uint max)
{
Span<uint> remainder = span[ClampReduce(span, min, max)..];
if (remainder.Length > 0)
{
ref uint remainderStart = ref MemoryMarshal.GetReference(remainder);
ref uint remainderEnd = ref Unsafe.Add(ref remainderStart, (uint)remainder.Length);
while (Unsafe.IsAddressLessThan(ref remainderStart, ref remainderEnd))
{
remainderStart = Clamp(remainderStart, min, max);
remainderStart = ref Unsafe.Add(ref remainderStart, 1);
}
}
}
=> TensorPrimitives.Clamp(span, min, max, span);
/// <summary>
/// Clamps the span values to the inclusive range of min and max.
@ -378,22 +350,7 @@ internal static class Numerics
/// <param name="max">The maximum inclusive value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(Span<int> span, int min, int max)
{
Span<int> remainder = span[ClampReduce(span, min, max)..];
if (remainder.Length > 0)
{
ref int remainderStart = ref MemoryMarshal.GetReference(remainder);
ref int remainderEnd = ref Unsafe.Add(ref remainderStart, (uint)remainder.Length);
while (Unsafe.IsAddressLessThan(ref remainderStart, ref remainderEnd))
{
remainderStart = Clamp(remainderStart, min, max);
remainderStart = ref Unsafe.Add(ref remainderStart, 1);
}
}
}
=> TensorPrimitives.Clamp(span, min, max, span);
/// <summary>
/// Clamps the span values to the inclusive range of min and max.
@ -403,22 +360,7 @@ internal static class Numerics
/// <param name="max">The maximum inclusive value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(Span<float> span, float min, float max)
{
Span<float> remainder = span[ClampReduce(span, min, max)..];
if (remainder.Length > 0)
{
ref float remainderStart = ref MemoryMarshal.GetReference(remainder);
ref float remainderEnd = ref Unsafe.Add(ref remainderStart, (uint)remainder.Length);
while (Unsafe.IsAddressLessThan(ref remainderStart, ref remainderEnd))
{
remainderStart = Clamp(remainderStart, min, max);
remainderStart = ref Unsafe.Add(ref remainderStart, 1);
}
}
}
=> TensorPrimitives.Clamp(span, min, max, span);
/// <summary>
/// Clamps the span values to the inclusive range of min and max.
@ -428,92 +370,12 @@ internal static class Numerics
/// <param name="max">The maximum inclusive value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(Span<double> span, double min, double max)
{
Span<double> remainder = span[ClampReduce(span, min, max)..];
if (remainder.Length > 0)
{
ref double remainderStart = ref MemoryMarshal.GetReference(remainder);
ref double remainderEnd = ref Unsafe.Add(ref remainderStart, (uint)remainder.Length);
while (Unsafe.IsAddressLessThan(ref remainderStart, ref remainderEnd))
{
remainderStart = Clamp(remainderStart, min, max);
remainderStart = ref Unsafe.Add(ref remainderStart, 1);
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int ClampReduce<T>(Span<T> span, T min, T max)
where T : unmanaged
{
if (Vector.IsHardwareAccelerated && span.Length >= Vector<T>.Count)
{
int remainder = ModuloP2(span.Length, Vector<T>.Count);
int adjustedCount = span.Length - remainder;
if (adjustedCount > 0)
{
ClampImpl(span[..adjustedCount], min, max);
}
return adjustedCount;
}
return 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ClampImpl<T>(Span<T> span, T min, T max)
where T : unmanaged
{
ref T sRef = ref MemoryMarshal.GetReference(span);
Vector<T> vmin = new(min);
Vector<T> vmax = new(max);
nint n = (nint)(uint)span.Length / Vector<T>.Count;
nint m = Modulo4(n);
nint u = n - m;
ref Vector<T> vs0 = ref Unsafe.As<T, Vector<T>>(ref MemoryMarshal.GetReference(span));
ref Vector<T> vs1 = ref Unsafe.Add(ref vs0, 1);
ref Vector<T> vs2 = ref Unsafe.Add(ref vs0, 2);
ref Vector<T> vs3 = ref Unsafe.Add(ref vs0, 3);
ref Vector<T> vsEnd = ref Unsafe.Add(ref vs0, u);
while (Unsafe.IsAddressLessThan(ref vs0, ref vsEnd))
{
vs0 = Vector.Min(Vector.Max(vmin, vs0), vmax);
vs1 = Vector.Min(Vector.Max(vmin, vs1), vmax);
vs2 = Vector.Min(Vector.Max(vmin, vs2), vmax);
vs3 = Vector.Min(Vector.Max(vmin, vs3), vmax);
vs0 = ref Unsafe.Add(ref vs0, 4);
vs1 = ref Unsafe.Add(ref vs1, 4);
vs2 = ref Unsafe.Add(ref vs2, 4);
vs3 = ref Unsafe.Add(ref vs3, 4);
}
if (m > 0)
{
vs0 = ref vsEnd;
vsEnd = ref Unsafe.Add(ref vsEnd, m);
while (Unsafe.IsAddressLessThan(ref vs0, ref vsEnd))
{
vs0 = Vector.Min(Vector.Max(vmin, vs0), vmax);
vs0 = ref Unsafe.Add(ref vs0, 1);
}
}
}
=> TensorPrimitives.Clamp(span, min, max, span);
/// <summary>
/// Pre-multiplies the "x", "y", "z" components of a vector by its "w" component leaving the "w" component intact.
/// </summary>
/// <param name="source">The <see cref="Vector4"/> to premultiply</param>
/// <param name="source">The <see cref="Vector4"/> to premultiply.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Premultiply(ref Vector4 source)
{
@ -524,50 +386,98 @@ internal static class Numerics
}
/// <summary>
/// Bulk variant of <see cref="Premultiply(ref Vector4)"/>
/// Clamps associated color components to the alpha component while preserving alpha.
/// </summary>
/// <param name="vectors">The span of vectors</param>
/// <param name="source">The associated vector to clamp.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ClampRgbToAlpha(ref Vector4 source)
{
Vector4 alpha = PermuteW(source);
source = WithW(Vector4.Min(Vector4.Max(source, Vector4.Zero), alpha), alpha);
}
/// <summary>
/// Premultiplies the X, Y, and Z components of each vector by its W component while preserving W.
/// </summary>
/// <param name="vectors">The vectors to premultiply.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Premultiply(Span<Vector4> vectors)
{
if (Avx.IsSupported && vectors.Length >= 2)
if (Vector512.IsHardwareAccelerated)
{
// Divide by 2 as 4 elements per Vector4 and 8 per Vector256<float>
int vectorsPerVector = Vector512<float>.Count / Vector128<float>.Count;
ref Vector512<float> vectorsBase = ref Unsafe.As<Vector4, Vector512<float>>(ref MemoryMarshal.GetReference(vectors));
ref Vector512<float> vectorsEnd = ref Unsafe.Add(ref vectorsBase, (uint)(vectors.Length / vectorsPerVector));
Vector128<float> alphaMask128 = Vector128.Create(0, 0, 0, -1).AsSingle();
Vector256<float> alphaMask256 = Vector256.Create(alphaMask128, alphaMask128);
Vector512<float> alphaMask = Vector512.Create(alphaMask256, alphaMask256);
while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsEnd))
{
Vector512<float> source = vectorsBase;
Vector512<float> alpha = Vector512_.ShuffleNative(source, ShuffleAlphaControl);
// Multiplication also squares W, so select the original W lanes to preserve alpha bit-for-bit.
vectorsBase = Vector512.ConditionalSelect(alphaMask, source, source * alpha);
vectorsBase = ref Unsafe.Add(ref vectorsBase, 1);
}
vectors = vectors[(vectors.Length - (vectors.Length % vectorsPerVector))..];
}
if (Vector256.IsHardwareAccelerated)
{
int vectorsPerVector = Vector256<float>.Count / Vector128<float>.Count;
ref Vector256<float> vectorsBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(vectors));
ref Vector256<float> vectorsLast = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length / 2u);
ref Vector256<float> vectorsEnd = ref Unsafe.Add(ref vectorsBase, (uint)(vectors.Length / vectorsPerVector));
Vector128<float> alphaMask128 = Vector128.Create(0, 0, 0, -1).AsSingle();
Vector256<float> alphaMask = Vector256.Create(alphaMask128, alphaMask128);
while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsLast))
while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsEnd))
{
Vector256<float> source = vectorsBase;
Vector256<float> alpha = Avx.Permute(source, ShuffleAlphaControl);
vectorsBase = Avx.Blend(Avx.Multiply(source, alpha), source, BlendAlphaControl);
Vector256<float> alpha = Vector256_.ShuffleNative(source, ShuffleAlphaControl);
// Multiplication also squares W, so select the original W lanes to preserve alpha bit-for-bit.
vectorsBase = Vector256.ConditionalSelect(alphaMask, source, source * alpha);
vectorsBase = ref Unsafe.Add(ref vectorsBase, 1);
}
if (Modulo2(vectors.Length) != 0)
{
// Vector4 fits neatly in pairs. Any overlap has to be equal to 1.
Premultiply(ref MemoryMarshal.GetReference(vectors[^1..]));
}
vectors = vectors[(vectors.Length - (vectors.Length % vectorsPerVector))..];
}
else
if (Vector128.IsHardwareAccelerated)
{
ref Vector4 vectorsStart = ref MemoryMarshal.GetReference(vectors);
ref Vector4 vectorsEnd = ref Unsafe.Add(ref vectorsStart, (uint)vectors.Length);
ref Vector128<float> vectorsBase = ref Unsafe.As<Vector4, Vector128<float>>(ref MemoryMarshal.GetReference(vectors));
ref Vector128<float> vectorsEnd = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length);
Vector128<float> alphaMask = Vector128.Create(0, 0, 0, -1).AsSingle();
while (Unsafe.IsAddressLessThan(ref vectorsStart, ref vectorsEnd))
while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsEnd))
{
Premultiply(ref vectorsStart);
Vector128<float> source = vectorsBase;
Vector128<float> alpha = Vector128_.ShuffleNative(source, ShuffleAlphaControl);
vectorsStart = ref Unsafe.Add(ref vectorsStart, 1);
// Multiplication also squares W, so select the original W lane to preserve alpha bit-for-bit.
vectorsBase = Vector128.ConditionalSelect(alphaMask, source, source * alpha);
vectorsBase = ref Unsafe.Add(ref vectorsBase, 1);
}
return;
}
ref Vector4 vectorsStart = ref MemoryMarshal.GetReference(vectors);
for (nuint i = 0; i < (uint)vectors.Length; i++)
{
Premultiply(ref Unsafe.Add(ref vectorsStart, i));
}
}
/// <summary>
/// Reverses the result of premultiplying a vector via <see cref="Premultiply(ref Vector4)"/>.
/// When alpha is zero, the RGB components remain unchanged because no unassociated value can be recovered.
/// </summary>
/// <param name="source">The <see cref="Vector4"/> to premultiply</param>
/// <param name="source">The <see cref="Vector4"/> to unpremultiply.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UnPremultiply(ref Vector4 source)
{
@ -575,74 +485,147 @@ internal static class Numerics
UnPremultiply(ref source, alpha);
}
/// <summary>
/// Unpremultiplies the X, Y, and Z components of a vector by the supplied alpha while preserving W.
/// When alpha is zero, the RGB components remain unchanged because no unassociated value can be recovered.
/// </summary>
/// <param name="source">The vector to unpremultiply.</param>
/// <param name="alpha">The source alpha replicated to every component.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UnPremultiply(ref Vector4 source, Vector4 alpha)
{
// Zero alpha has no mathematical inverse, so preserve stored additive or hidden RGB data unchanged.
if (alpha == Vector4.Zero)
{
return;
}
// Divide source by alpha if alpha is nonzero, otherwise set all components to match the source value
// Blend the result with the alpha vector to ensure that the alpha component is unchanged
// Division would replace W with one, so restore the original alpha component exactly.
source = WithW(source / alpha, alpha);
}
/// <summary>
/// Bulk variant of <see cref="UnPremultiply(ref Vector4)"/>
/// Unpremultiplies the X, Y, and Z components of each vector by its W component while preserving W.
/// Vectors with zero W retain their RGB components because no unassociated value can be recovered.
/// </summary>
/// <param name="vectors">The span of vectors</param>
/// <param name="vectors">The vectors to unpremultiply.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void UnPremultiply(Span<Vector4> vectors)
{
if (Avx.IsSupported && vectors.Length >= 2)
if (Vector512.IsHardwareAccelerated)
{
// Divide by 2 as 4 elements per Vector4 and 8 per Vector256<float>
ref Vector256<float> vectorsBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(vectors));
ref Vector256<float> vectorsLast = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length / 2u);
Vector256<float> epsilon = Vector256.Create(Constants.Epsilon);
int vectorsPerVector = Vector512<float>.Count / Vector128<float>.Count;
ref Vector512<float> vectorsBase = ref Unsafe.As<Vector4, Vector512<float>>(ref MemoryMarshal.GetReference(vectors));
ref Vector512<float> vectorsEnd = ref Unsafe.Add(ref vectorsBase, (uint)(vectors.Length / vectorsPerVector));
while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsLast))
while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsEnd))
{
Vector256<float> source = vectorsBase;
Vector256<float> alpha = Avx.Permute(source, ShuffleAlphaControl);
Vector512<float> source = vectorsBase;
Vector512<float> alpha = Vector512_.ShuffleNative(source, ShuffleAlphaControl);
vectorsBase = UnPremultiply(source, alpha);
vectorsBase = ref Unsafe.Add(ref vectorsBase, 1);
}
if (Modulo2(vectors.Length) != 0)
vectors = vectors[(vectors.Length - (vectors.Length % vectorsPerVector))..];
}
if (Vector256.IsHardwareAccelerated)
{
int vectorsPerVector = Vector256<float>.Count / Vector128<float>.Count;
ref Vector256<float> vectorsBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(vectors));
ref Vector256<float> vectorsEnd = ref Unsafe.Add(ref vectorsBase, (uint)(vectors.Length / vectorsPerVector));
while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsEnd))
{
// Vector4 fits neatly in pairs. Any overlap has to be equal to 1.
UnPremultiply(ref MemoryMarshal.GetReference(vectors[^1..]));
Vector256<float> source = vectorsBase;
Vector256<float> alpha = Vector256_.ShuffleNative(source, ShuffleAlphaControl);
vectorsBase = UnPremultiply(source, alpha);
vectorsBase = ref Unsafe.Add(ref vectorsBase, 1);
}
vectors = vectors[(vectors.Length - (vectors.Length % vectorsPerVector))..];
}
else
if (Vector128.IsHardwareAccelerated)
{
ref Vector4 vectorsStart = ref MemoryMarshal.GetReference(vectors);
ref Vector4 vectorsEnd = ref Unsafe.Add(ref vectorsStart, (uint)vectors.Length);
ref Vector128<float> vectorsBase = ref Unsafe.As<Vector4, Vector128<float>>(ref MemoryMarshal.GetReference(vectors));
ref Vector128<float> vectorsEnd = ref Unsafe.Add(ref vectorsBase, (uint)vectors.Length);
while (Unsafe.IsAddressLessThan(ref vectorsStart, ref vectorsEnd))
while (Unsafe.IsAddressLessThan(ref vectorsBase, ref vectorsEnd))
{
UnPremultiply(ref vectorsStart);
vectorsStart = ref Unsafe.Add(ref vectorsStart, 1);
Vector128<float> source = vectorsBase;
Vector128<float> alpha = Vector128_.ShuffleNative(source, ShuffleAlphaControl);
vectorsBase = UnPremultiply(source, alpha);
vectorsBase = ref Unsafe.Add(ref vectorsBase, 1);
}
return;
}
ref Vector4 vectorsStart = ref MemoryMarshal.GetReference(vectors);
for (nuint i = 0; i < (uint)vectors.Length; i++)
{
UnPremultiply(ref Unsafe.Add(ref vectorsStart, i));
}
}
/// <summary>
/// Unpremultiplies the RGB lanes of a vector while preserving its alpha lane.
/// When alpha is zero, the RGB lanes remain unchanged because no unassociated value can be recovered.
/// </summary>
/// <param name="source">The associated vector.</param>
/// <param name="alpha">The source alpha replicated to every lane.</param>
/// <returns>The unassociated vector.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<float> UnPremultiply(Vector128<float> source, Vector128<float> alpha)
{
// Zero alpha has no mathematical inverse, so select the source lanes to preserve stored additive or hidden RGB data.
Vector128<float> zeroMask = Vector128.Equals(alpha, Vector128<float>.Zero);
Vector128<float> result = Vector128.ConditionalSelect(zeroMask, source, source / alpha);
// Division would replace W with one, so restore the original alpha lane exactly.
Vector128<float> alphaMask = Vector128.Create(0, 0, 0, -1).AsSingle();
return Vector128.ConditionalSelect(alphaMask, alpha, result);
}
/// <summary>
/// Unpremultiplies the RGB lanes of two vectors while preserving their alpha lanes.
/// Vectors with zero alpha retain their RGB lanes because no unassociated value can be recovered.
/// </summary>
/// <param name="source">The associated vectors.</param>
/// <param name="alpha">Each source alpha replicated across its four lanes.</param>
/// <returns>The unassociated vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<float> UnPremultiply(Vector256<float> source, Vector256<float> alpha)
{
// Check if alpha is zero to avoid division by zero
// Zero alpha has no mathematical inverse, so select the source lanes to preserve stored additive or hidden RGB data.
Vector256<float> zeroMask = Avx.CompareEqual(alpha, Vector256<float>.Zero);
// Divide source by alpha if alpha is nonzero, otherwise set all components to match the source value
Vector256<float> result = Avx.BlendVariable(Avx.Divide(source, alpha), source, zeroMask);
// Blend the result with the alpha vector to ensure that the alpha component is unchanged
// Division would replace W with one, so restore both original alpha lanes exactly.
return Avx.Blend(result, alpha, BlendAlphaControl);
}
/// <summary>
/// Unpremultiplies the RGB lanes of four vectors while preserving their alpha lanes.
/// Vectors with zero alpha retain their RGB lanes because no unassociated value can be recovered.
/// </summary>
/// <param name="source">The associated vectors.</param>
/// <param name="alpha">Each source alpha replicated across its four lanes.</param>
/// <returns>The unassociated vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<float> UnPremultiply(Vector512<float> source, Vector512<float> alpha)
{
// Zero alpha has no mathematical inverse, so select the source lanes to preserve stored additive or hidden RGB data.
Vector512<float> zeroMask = Vector512.Equals(alpha, Vector512<float>.Zero);
Vector512<float> result = Vector512.ConditionalSelect(zeroMask, source, source / alpha);
// Division would replace W with one, so restore all four original alpha lanes exactly.
Vector512<float> alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle();
return Vector512.ConditionalSelect(alphaMask, alpha, result);
}
/// <summary>
/// Permutes the given vector return a new instance with all the values set to <see cref="Vector4.W"/>.
/// </summary>
@ -690,7 +673,7 @@ internal static class Numerics
/// </summary>
/// <param name="vectors">The span of vectors</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe void CubePowOnXYZ(Span<Vector4> vectors)
public static void CubePowOnXYZ(Span<Vector4> vectors)
{
ref Vector4 baseRef = ref MemoryMarshal.GetReference(vectors);
ref Vector4 endRef = ref Unsafe.Add(ref baseRef, (uint)vectors.Length);
@ -957,20 +940,10 @@ internal static class Numerics
/// <typeparam name="TVector">The type of the vector.</typeparam>
/// <param name="span">The given span.</param>
/// <returns>Count of vectors that safely fit into the span.</returns>
public static nuint VectorCount<TVector>(this Span<byte> span)
public static nuint VectorCount<TVector>(this ReadOnlySpan<byte> span)
where TVector : struct
=> (uint)span.Length / (uint)Vector<TVector>.Count;
/// <summary>
/// Gets the count of vectors that safely fit into the given span.
/// </summary>
/// <typeparam name="TVector">The type of the vector.</typeparam>
/// <param name="span">The given span.</param>
/// <returns>Count of vectors that safely fit into the span.</returns>
public static nuint Vector128Count<TVector>(this Span<byte> span)
where TVector : struct
=> (uint)span.Length / (uint)Vector128<TVector>.Count;
/// <summary>
/// Gets the count of vectors that safely fit into the given span.
/// </summary>
@ -981,16 +954,6 @@ internal static class Numerics
where TVector : struct
=> (uint)span.Length / (uint)Vector128<TVector>.Count;
/// <summary>
/// Gets the count of vectors that safely fit into the given span.
/// </summary>
/// <typeparam name="TVector">The type of the vector.</typeparam>
/// <param name="span">The given span.</param>
/// <returns>Count of vectors that safely fit into the span.</returns>
public static nuint Vector256Count<TVector>(this Span<byte> span)
where TVector : struct
=> (uint)span.Length / (uint)Vector256<TVector>.Count;
/// <summary>
/// Gets the count of vectors that safely fit into the given span.
/// </summary>
@ -1001,16 +964,6 @@ internal static class Numerics
where TVector : struct
=> (uint)span.Length / (uint)Vector256<TVector>.Count;
/// <summary>
/// Gets the count of vectors that safely fit into the given span.
/// </summary>
/// <typeparam name="TVector">The type of the vector.</typeparam>
/// <param name="span">The given span.</param>
/// <returns>Count of vectors that safely fit into the span.</returns>
public static nuint Vector512Count<TVector>(this Span<byte> span)
where TVector : struct
=> (uint)span.Length / (uint)Vector512<TVector>.Count;
/// <summary>
/// Gets the count of vectors that safely fit into the given span.
/// </summary>
@ -1027,7 +980,7 @@ internal static class Numerics
/// <typeparam name="TVector">The type of the vector.</typeparam>
/// <param name="span">The given span.</param>
/// <returns>Count of vectors that safely fit into the span.</returns>
public static nuint VectorCount<TVector>(this Span<float> span)
public static nuint VectorCount<TVector>(this ReadOnlySpan<float> span)
where TVector : struct
=> (uint)span.Length / (uint)Vector<TVector>.Count;
@ -1037,7 +990,7 @@ internal static class Numerics
/// <typeparam name="TVector">The type of the vector.</typeparam>
/// <param name="span">The given span.</param>
/// <returns>Count of vectors that safely fit into the span.</returns>
public static nuint Vector128Count<TVector>(this Span<float> span)
public static nuint Vector128Count<TVector>(this ReadOnlySpan<float> span)
where TVector : struct
=> (uint)span.Length / (uint)Vector128<TVector>.Count;
@ -1047,7 +1000,7 @@ internal static class Numerics
/// <typeparam name="TVector">The type of the vector.</typeparam>
/// <param name="span">The given span.</param>
/// <returns>Count of vectors that safely fit into the span.</returns>
public static nuint Vector256Count<TVector>(this Span<float> span)
public static nuint Vector256Count<TVector>(this ReadOnlySpan<float> span)
where TVector : struct
=> (uint)span.Length / (uint)Vector256<TVector>.Count;
@ -1067,7 +1020,7 @@ internal static class Numerics
/// <typeparam name="TVector">The type of the vector.</typeparam>
/// <param name="span">The given span.</param>
/// <returns>Count of vectors that safely fit into the span.</returns>
public static nuint Vector512Count<TVector>(this Span<float> span)
public static nuint Vector512Count<TVector>(this ReadOnlySpan<float> span)
where TVector : struct
=> (uint)span.Length / (uint)Vector512<TVector>.Count;

32
src/ImageSharp/Common/Helpers/Shuffle/IComponentShuffle.cs

@ -1,36 +1,26 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
// The JIT can detect and optimize rotation idioms ROTL (Rotate Left)
// and ROTR (Rotate Right) emitting efficient CPU instructions:
// https://github.com/dotnet/coreclr/pull/1830
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp;
/// <summary>
/// Defines the contract for methods that allow the shuffling of pixel components.
/// Used for shuffling on platforms that do not support Hardware Intrinsics.
/// Defines a stateless operation over packed pixel components.
/// </summary>
internal interface IComponentShuffle
{
/// <summary>
/// Shuffles then slices 8-bit integers in <paramref name="source"/>
/// using a byte control and store the results in <paramref name="destination"/>.
/// If successful, this method will reduce the length of <paramref name="source"/> length
/// by the shuffle amount.
/// Reorders one packed pixel.
/// </summary>
/// <param name="source">The source span of bytes.</param>
/// <param name="destination">The destination span of bytes.</param>
void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination);
/// <param name="source">The source components, with the first component in the least-significant byte.</param>
/// <returns>The reordered packed components.</returns>
public static abstract uint Invoke(uint source);
/// <summary>
/// Shuffle 8-bit integers in <paramref name="source"/>
/// using the control and store the results in <paramref name="destination"/>.
/// Reorders the packed pixels in a 128-bit vector.
/// </summary>
/// <param name="source">The source span of bytes.</param>
/// <param name="destination">The destination span of bytes.</param>
/// <remarks>
/// Implementation can assume that source.Length is less or equal than destination.Length.
/// Loops should iterate using source.Length.
/// </remarks>
void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination);
/// <param name="source">The source pixels.</param>
/// <returns>The reordered pixels.</returns>
public static abstract Vector128<byte> Invoke(Vector128<byte> source);
}

136
src/ImageSharp/Common/Helpers/Shuffle/IPad3Shuffle4.cs

@ -1,85 +1,95 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static SixLabors.ImageSharp.SimdUtils;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp;
/// <inheritdoc/>
/// <summary>
/// Defines a stateless operation that reorders a three-component pixel after adding opaque alpha.
/// </summary>
internal interface IPad3Shuffle4 : IComponentShuffle
{
}
internal readonly struct DefaultPad3Shuffle4([ConstantExpected] byte control) : IPad3Shuffle4
/// <summary>
/// Preserves XYZ order and appends opaque W.
/// </summary>
internal readonly struct XYZWPad3Shuffle4 : IPad3Shuffle4
{
public byte Control { get; } = control;
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
#pragma warning disable CA1857 // A constant is expected for the parameter
=> HwIntrinsics.Pad3Shuffle4Reduce(ref source, ref destination, this.Control);
#pragma warning restore CA1857 // A constant is expected for the parameter
public static uint Invoke(uint source) => source;
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source) => source;
}
/// <summary>
/// Reorders padded XYZW components to WXYZ.
/// </summary>
internal readonly struct WXYZPad3Shuffle4 : IPad3Shuffle4
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
{
ref byte sBase = ref MemoryMarshal.GetReference(source);
ref byte dBase = ref MemoryMarshal.GetReference(destination);
SimdUtils.Shuffle.InverseMMShuffle(this.Control, out uint p3, out uint p2, out uint p1, out uint p0);
Span<byte> temp = stackalloc byte[4];
ref byte t = ref MemoryMarshal.GetReference(temp);
ref uint tu = ref Unsafe.As<byte, uint>(ref t);
for (nuint i = 0, j = 0; i < (uint)source.Length; i += 3, j += 4)
{
ref byte s = ref Unsafe.Add(ref sBase, i);
tu = Unsafe.As<byte, uint>(ref s) | 0xFF000000;
Unsafe.Add(ref dBase, j + 0) = Unsafe.Add(ref t, p0);
Unsafe.Add(ref dBase, j + 1) = Unsafe.Add(ref t, p1);
Unsafe.Add(ref dBase, j + 2) = Unsafe.Add(ref t, p2);
Unsafe.Add(ref dBase, j + 3) = Unsafe.Add(ref t, p3);
}
}
public static uint Invoke(uint source)
// The scalar pipeline has already appended opaque W, so the four-component
// WXYZ operator performs the complete remaining permutation.
=> WXYZShuffle4.Invoke(source);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
// Each four-byte group is an XYZW pixel with opaque W. Selecting [3, 0, 1, 2]
// produces WXYZ, and offsets 4, 8, and 12 repeat that rotation for the next pixels.
=> Vector128.ShuffleNative(source, Vector128.Create((byte)3, 0, 1, 2, 7, 4, 5, 6, 11, 8, 9, 10, 15, 12, 13, 14));
}
internal readonly struct XYZWPad3Shuffle4 : IPad3Shuffle4
/// <summary>
/// Reorders padded XYZW components to WZYX.
/// </summary>
internal readonly struct WZYXPad3Shuffle4 : IPad3Shuffle4
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
=> HwIntrinsics.Pad3Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle3210);
public static uint Invoke(uint source)
// The scalar pipeline has already appended opaque W, so the four-component
// WZYX operator performs the complete remaining permutation.
=> WZYXShuffle4.Invoke(source);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
// Each four-byte group is an XYZW pixel with opaque W. Selecting [3, 2, 1, 0]
// produces WZYX, and offsets 4, 8, and 12 repeat that reversal for the next pixels.
=> Vector128.ShuffleNative(source, Vector128.Create((byte)3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8, 15, 14, 13, 12));
}
/// <summary>
/// Reorders padded XYZW components to ZYXW.
/// </summary>
internal readonly struct ZYXWPad3Shuffle4 : IPad3Shuffle4
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
{
ref byte sBase = ref MemoryMarshal.GetReference(source);
ref byte dBase = ref MemoryMarshal.GetReference(destination);
ref byte sEnd = ref Unsafe.Add(ref sBase, (uint)source.Length);
ref byte sLoopEnd = ref Unsafe.Subtract(ref sEnd, 4);
while (Unsafe.IsAddressLessThan(ref sBase, ref sLoopEnd))
{
Unsafe.As<byte, uint>(ref dBase) = Unsafe.As<byte, uint>(ref sBase) | 0xFF000000;
sBase = ref Unsafe.Add(ref sBase, 3);
dBase = ref Unsafe.Add(ref dBase, 4);
}
while (Unsafe.IsAddressLessThan(ref sBase, ref sEnd))
{
Unsafe.Add(ref dBase, 0) = Unsafe.Add(ref sBase, 0);
Unsafe.Add(ref dBase, 1) = Unsafe.Add(ref sBase, 1);
Unsafe.Add(ref dBase, 2) = Unsafe.Add(ref sBase, 2);
Unsafe.Add(ref dBase, 3) = byte.MaxValue;
sBase = ref Unsafe.Add(ref sBase, 3);
dBase = ref Unsafe.Add(ref dBase, 4);
}
}
public static uint Invoke(uint source)
// The scalar pipeline has already appended opaque W, so the four-component
// ZYXW operator performs the complete remaining permutation.
=> ZYXWShuffle4.Invoke(source);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
// Each four-byte group is an XYZW pixel with opaque W. Selecting [2, 1, 0, 3]
// exchanges X and Z to produce ZYXW, with offsets 4, 8, and 12 covering the next pixels.
=> Vector128.ShuffleNative(source, Vector128.Create((byte)2, 1, 0, 3, 6, 5, 4, 7, 10, 9, 8, 11, 14, 13, 12, 15));
}

45
src/ImageSharp/Common/Helpers/Shuffle/IShuffle3.cs

@ -1,41 +1,38 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static SixLabors.ImageSharp.SimdUtils;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp;
/// <inheritdoc/>
/// <summary>
/// Identifies a stateless three-component shuffle operator.
/// </summary>
internal interface IShuffle3 : IComponentShuffle
{
}
internal readonly struct DefaultShuffle3([ConstantExpected] byte control) : IShuffle3
/// <summary>
/// Reorders XYZ components to ZYX.
/// </summary>
internal readonly struct ZYXShuffle3 : IShuffle3
{
public byte Control { get; } = control;
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
#pragma warning disable CA1857 // A constant is expected for the parameter
=> HwIntrinsics.Shuffle3Reduce(ref source, ref destination, this.Control);
#pragma warning restore CA1857 // A constant is expected for the parameter
public static uint Invoke(uint source)
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
{
ref byte sBase = ref MemoryMarshal.GetReference(source);
ref byte dBase = ref MemoryMarshal.GetReference(destination);
// The scalar tail is staged as XYZW with an unused W byte. Reusing the four-component
// ZYXW operator produces ZYX in the low three bytes consumed by the caller.
=> ZYXWShuffle4.Invoke(source);
SimdUtils.Shuffle.InverseMMShuffle(this.Control, out _, out uint p2, out uint p1, out uint p0);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
for (nuint i = 0; i < (uint)source.Length; i += 3)
{
Unsafe.Add(ref dBase, i + 0) = Unsafe.Add(ref sBase, p0 + i);
Unsafe.Add(ref dBase, i + 1) = Unsafe.Add(ref sBase, p1 + i);
Unsafe.Add(ref dBase, i + 2) = Unsafe.Add(ref sBase, p2 + i);
}
}
// Each four-byte group is a temporary XYZW pixel created by the shuffle pipeline.
// Selecting [2, 1, 0, 3] produces ZYXW, and offsets 4, 8, and 12 repeat that
// permutation for the next pixels. The pipeline subsequently discards every W byte.
=> Vector128.ShuffleNative(source, Vector128.Create((byte)2, 1, 0, 3, 6, 5, 4, 7, 10, 9, 8, 11, 14, 13, 12, 15));
}

354
src/ImageSharp/Common/Helpers/Shuffle/IShuffle4.cs

@ -2,177 +2,287 @@
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static SixLabors.ImageSharp.SimdUtils;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp;
/// <inheritdoc/>
/// <summary>
/// Defines a stateless operation over one packed four-component pixel.
/// </summary>
internal interface IShuffle4 : IComponentShuffle
{
}
internal readonly struct DefaultShuffle4([ConstantExpected] byte control) : IShuffle4
{
public byte Control { get; } = control;
/// <summary>
/// Reorders the packed pixels in a 256-bit vector.
/// </summary>
/// <param name="source">The source pixels.</param>
/// <returns>The reordered pixels.</returns>
public static abstract Vector256<byte> Invoke(Vector256<byte> source);
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
#pragma warning disable CA1857 // A constant is expected for the parameter
=> HwIntrinsics.Shuffle4Reduce(ref source, ref destination, this.Control);
#pragma warning restore CA1857 // A constant is expected for the parameter
/// <summary>
/// Reorders the packed pixels in a 512-bit vector.
/// </summary>
/// <param name="source">The source pixels.</param>
/// <returns>The reordered pixels.</returns>
public static abstract Vector512<byte> Invoke(Vector512<byte> source);
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
/// <summary>
/// Expands one 128-bit lane mask into absolute indices for a 512-bit shuffle.
/// </summary>
/// <param name="laneMask">The indices, from zero through fifteen, for one 128-bit lane.</param>
/// <returns>The corresponding absolute indices for all four 128-bit lanes.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<byte> ExpandLaneMask(Vector128<byte> laneMask)
{
ref byte sBase = ref MemoryMarshal.GetReference(source);
ref byte dBase = ref MemoryMarshal.GetReference(destination);
SimdUtils.Shuffle.InverseMMShuffle(this.Control, out uint p3, out uint p2, out uint p1, out uint p0);
for (nuint i = 0; i < (uint)source.Length; i += 4)
{
Unsafe.Add(ref dBase, i + 0) = Unsafe.Add(ref sBase, p0 + i);
Unsafe.Add(ref dBase, i + 1) = Unsafe.Add(ref sBase, p1 + i);
Unsafe.Add(ref dBase, i + 2) = Unsafe.Add(ref sBase, p2 + i);
Unsafe.Add(ref dBase, i + 3) = Unsafe.Add(ref sBase, p3 + i);
}
// A 512-bit vector contains four 128-bit lanes, and each lane contains four packed
// XYZW pixels. The supplied mask addresses bytes 0..15 in the first lane. The managed
// Vector512.Shuffle fallback addresses the complete 64-byte vector, so the same
// permutation must address bytes 16..31, 32..47, and 48..63 in the remaining lanes.
//
// AVX-512BW VPSHUFB instead interprets indices independently within each 128-bit lane
// and uses only the low four bits to select a byte. Adding the lane offsets therefore
// satisfies the managed absolute-index contract without changing the native lane-local
// permutation.
Vector128<byte> lane1 = laneMask + Vector128.Create((byte)16);
Vector128<byte> lane2 = laneMask + Vector128.Create((byte)32);
Vector128<byte> lane3 = laneMask + Vector128.Create((byte)48);
return Vector512.Create(Vector256.Create(laneMask, lane1), Vector256.Create(lane2, lane3));
}
}
/// <summary>
/// Reorders XYZW components to WXYZ.
/// </summary>
internal readonly struct WXYZShuffle4 : IShuffle4
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
=> HwIntrinsics.Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle2103);
public static uint Invoke(uint source)
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
// source = [W Z Y X]
// ROTL(8, source) = [Z Y X W]
=> BitOperations.RotateLeft(source, 8);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
=> Vector128.ShuffleNative(source, CreateLaneMask());
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<byte> Invoke(Vector256<byte> source)
{
ref uint sBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(source));
ref uint dBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(destination));
uint n = (uint)source.Length / 4;
for (nuint i = 0; i < n; i++)
{
uint packed = Unsafe.Add(ref sBase, i);
// packed = [W Z Y X]
// ROTL(8, packed) = [Z Y X W]
Unsafe.Add(ref dBase, i) = (packed << 8) | (packed >> 24);
}
// AVX2 byte shuffles select within 128-bit lanes, so both halves use the same pixel-local indices.
Vector128<byte> mask = CreateLaneMask();
return Vector256_.ShufflePerLane(source, Vector256.Create(mask, mask));
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<byte> Invoke(Vector512<byte> source)
// Expand the four-pixel lane permutation across all four 128-bit lanes.
=> Vector512.ShuffleNative(source, IShuffle4.ExpandLaneMask(CreateLaneMask()));
/// <summary>
/// Creates the indices that rotate each XYZW pixel to WXYZ within one 128-bit lane.
/// </summary>
/// <returns>The pixel-local byte shuffle indices.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> CreateLaneMask()
// Each four-byte group is one XYZW pixel. Selecting [3, 0, 1, 2] produces
// WXYZ, and offsets 4, 8, and 12 repeat that permutation for the next pixels.
=> Vector128.Create((byte)3, 0, 1, 2, 7, 4, 5, 6, 11, 8, 9, 10, 15, 12, 13, 14);
}
/// <summary>
/// Reorders XYZW components to WZYX.
/// </summary>
internal readonly struct WZYXShuffle4 : IShuffle4
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
=> HwIntrinsics.Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle0123);
public static uint Invoke(uint source)
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
// source = [W Z Y X]
// REVERSE(source) = [X Y Z W]
=> BinaryPrimitives.ReverseEndianness(source);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
=> Vector128.ShuffleNative(source, CreateLaneMask());
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<byte> Invoke(Vector256<byte> source)
{
ref uint sBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(source));
ref uint dBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(destination));
uint n = (uint)source.Length / 4;
for (nuint i = 0; i < n; i++)
{
uint packed = Unsafe.Add(ref sBase, i);
// packed = [W Z Y X]
// REVERSE(packedArgb) = [X Y Z W]
Unsafe.Add(ref dBase, i) = BinaryPrimitives.ReverseEndianness(packed);
}
// AVX2 byte shuffles select within 128-bit lanes, so both halves use the same pixel-local indices.
Vector128<byte> mask = CreateLaneMask();
return Vector256_.ShufflePerLane(source, Vector256.Create(mask, mask));
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<byte> Invoke(Vector512<byte> source)
// Expand the four-pixel lane permutation across all four 128-bit lanes.
=> Vector512.ShuffleNative(source, IShuffle4.ExpandLaneMask(CreateLaneMask()));
/// <summary>
/// Creates the indices that reverse each XYZW pixel to WZYX within one 128-bit lane.
/// </summary>
/// <returns>The pixel-local byte shuffle indices.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> CreateLaneMask()
// Each four-byte group is one XYZW pixel. Selecting [3, 2, 1, 0] produces
// WZYX, and offsets 4, 8, and 12 repeat that reversal for the next pixels.
=> Vector128.Create((byte)3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8, 15, 14, 13, 12);
}
/// <summary>
/// Reorders XYZW components to YZWX.
/// </summary>
internal readonly struct YZWXShuffle4 : IShuffle4
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
=> HwIntrinsics.Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle0321);
public static uint Invoke(uint source)
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
// source = [W Z Y X]
// ROTR(8, source) = [X W Z Y]
=> BitOperations.RotateRight(source, 8);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
=> Vector128.ShuffleNative(source, CreateLaneMask());
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<byte> Invoke(Vector256<byte> source)
{
ref uint sBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(source));
ref uint dBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(destination));
uint n = (uint)source.Length / 4;
for (nuint i = 0; i < n; i++)
{
uint packed = Unsafe.Add(ref sBase, i);
// packed = [W Z Y X]
// ROTR(8, packedArgb) = [Y Z W X]
Unsafe.Add(ref dBase, i) = BitOperations.RotateRight(packed, 8);
}
// AVX2 byte shuffles select within 128-bit lanes, so both halves use the same pixel-local indices.
Vector128<byte> mask = CreateLaneMask();
return Vector256_.ShufflePerLane(source, Vector256.Create(mask, mask));
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<byte> Invoke(Vector512<byte> source)
// Expand the four-pixel lane permutation across all four 128-bit lanes.
=> Vector512.ShuffleNative(source, IShuffle4.ExpandLaneMask(CreateLaneMask()));
/// <summary>
/// Creates the indices that rotate each XYZW pixel to YZWX within one 128-bit lane.
/// </summary>
/// <returns>The pixel-local byte shuffle indices.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> CreateLaneMask()
// Each four-byte group is one XYZW pixel. Selecting [1, 2, 3, 0] produces
// YZWX, and offsets 4, 8, and 12 repeat that rotation for the next pixels.
=> Vector128.Create((byte)1, 2, 3, 0, 5, 6, 7, 4, 9, 10, 11, 8, 13, 14, 15, 12);
}
/// <summary>
/// Reorders XYZW components to ZYXW.
/// </summary>
internal readonly struct ZYXWShuffle4 : IShuffle4
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
=> HwIntrinsics.Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle3012);
public static uint Invoke(uint source)
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
// source = [W Z Y X]
// source & 0xFF00FF00 = [W 0 Y 0]
// ROTL(source & 0x00FF00FF) = [0 X 0 Z]
// combined = [W X Y Z]
=> (source & 0xFF00FF00) | BitOperations.RotateLeft(source & 0x00FF00FF, 16);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
=> Vector128.ShuffleNative(source, CreateLaneMask());
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<byte> Invoke(Vector256<byte> source)
{
ref uint sBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(source));
ref uint dBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(destination));
uint n = (uint)source.Length / 4;
for (nuint i = 0; i < n; i++)
{
uint packed = Unsafe.Add(ref sBase, i);
// packed = [W Z Y X]
// tmp1 = [W 0 Y 0]
// tmp2 = [0 Z 0 X]
// tmp3=ROTL(16, tmp2) = [0 X 0 Z]
// tmp1 + tmp3 = [W X Y Z]
uint tmp1 = packed & 0xFF00FF00;
uint tmp2 = packed & 0x00FF00FF;
uint tmp3 = BitOperations.RotateLeft(tmp2, 16);
Unsafe.Add(ref dBase, i) = tmp1 + tmp3;
}
// AVX2 byte shuffles select within 128-bit lanes, so both halves use the same pixel-local indices.
Vector128<byte> mask = CreateLaneMask();
return Vector256_.ShufflePerLane(source, Vector256.Create(mask, mask));
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<byte> Invoke(Vector512<byte> source)
// Expand the four-pixel lane permutation across all four 128-bit lanes.
=> Vector512.ShuffleNative(source, IShuffle4.ExpandLaneMask(CreateLaneMask()));
/// <summary>
/// Creates the indices that exchange X and Z in each XYZW pixel within one 128-bit lane.
/// </summary>
/// <returns>The pixel-local byte shuffle indices.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> CreateLaneMask()
// Each four-byte group is one XYZW pixel. Selecting [2, 1, 0, 3] exchanges
// X and Z to produce ZYXW, with offsets 4, 8, and 12 covering the next pixels.
=> Vector128.Create((byte)2, 1, 0, 3, 6, 5, 4, 7, 10, 9, 8, 11, 14, 13, 12, 15);
}
/// <summary>
/// Reorders XYZW components to XWZY.
/// </summary>
internal readonly struct XWZYShuffle4 : IShuffle4
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
=> HwIntrinsics.Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle1230);
public static uint Invoke(uint source)
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
// source = [W Z Y X]
// source & 0x00FF00FF = [0 Z 0 X]
// ROTL(source & 0xFF00FF00) = [Y 0 W 0]
// combined = [Y Z W X]
=> (source & 0x00FF00FF) | BitOperations.RotateLeft(source & 0xFF00FF00, 16);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
=> Vector128.ShuffleNative(source, CreateLaneMask());
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<byte> Invoke(Vector256<byte> source)
{
ref uint sBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(source));
ref uint dBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(destination));
uint n = (uint)source.Length / 4;
for (nuint i = 0; i < n; i++)
{
uint packed = Unsafe.Add(ref sBase, i);
// packed = [W Z Y X]
// tmp1 = [0 Z 0 X]
// tmp2 = [W 0 Y 0]
// tmp3=ROTL(16, tmp2) = [Y 0 W 0]
// tmp1 + tmp3 = [Y Z W X]
uint tmp1 = packed & 0x00FF00FF;
uint tmp2 = packed & 0xFF00FF00;
uint tmp3 = BitOperations.RotateLeft(tmp2, 16);
Unsafe.Add(ref dBase, i) = tmp1 + tmp3;
}
// AVX2 byte shuffles select within 128-bit lanes, so both halves use the same pixel-local indices.
Vector128<byte> mask = CreateLaneMask();
return Vector256_.ShufflePerLane(source, Vector256.Create(mask, mask));
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<byte> Invoke(Vector512<byte> source)
// Expand the four-pixel lane permutation across all four 128-bit lanes.
=> Vector512.ShuffleNative(source, IShuffle4.ExpandLaneMask(CreateLaneMask()));
/// <summary>
/// Creates the indices that exchange Y and W in each XYZW pixel within one 128-bit lane.
/// </summary>
/// <returns>The pixel-local byte shuffle indices.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> CreateLaneMask()
// Each four-byte group is one XYZW pixel. Selecting [0, 3, 2, 1] exchanges
// Y and W to produce XWZY, with offsets 4, 8, and 12 covering the next pixels.
=> Vector128.Create((byte)0, 3, 2, 1, 4, 7, 6, 5, 8, 11, 10, 9, 12, 15, 14, 13);
}

137
src/ImageSharp/Common/Helpers/Shuffle/IShuffle4Slice3.cs

@ -1,85 +1,106 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static SixLabors.ImageSharp.SimdUtils;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp;
/// <inheritdoc/>
/// <summary>
/// Defines a stateless operation that reorders four packed components before retaining three.
/// </summary>
internal interface IShuffle4Slice3 : IComponentShuffle
{
}
internal readonly struct DefaultShuffle4Slice3([ConstantExpected] byte control) : IShuffle4Slice3
/// <summary>
/// Preserves XYZ order and discards W.
/// </summary>
internal readonly struct XYZWShuffle4Slice3 : IShuffle4Slice3
{
public byte Control { get; } = control;
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
#pragma warning disable CA1857 // A constant is expected for the parameter
=> HwIntrinsics.Shuffle4Slice3Reduce(ref source, ref destination, this.Control);
#pragma warning restore CA1857 // A constant is expected for the parameter
public static uint Invoke(uint source) => source;
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source) => source;
}
/// <summary>
/// Reorders XYZW components to YZW before discarding X.
/// </summary>
internal readonly struct YZWXShuffle4Slice3 : IShuffle4Slice3
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
{
ref byte sBase = ref MemoryMarshal.GetReference(source);
ref byte dBase = ref MemoryMarshal.GetReference(destination);
SimdUtils.Shuffle.InverseMMShuffle(this.Control, out _, out uint p2, out uint p1, out uint p0);
for (nuint i = 0, j = 0; i < (uint)destination.Length; i += 3, j += 4)
{
Unsafe.Add(ref dBase, i + 0) = Unsafe.Add(ref sBase, p0 + j);
Unsafe.Add(ref dBase, i + 1) = Unsafe.Add(ref sBase, p1 + j);
Unsafe.Add(ref dBase, i + 2) = Unsafe.Add(ref sBase, p2 + j);
}
}
public static uint Invoke(uint source)
// Reuse the four-component rotation; the caller stores only the low YZW
// bytes and therefore discards the rotated X byte.
=> YZWXShuffle4.Invoke(source);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
// Each four-byte group is an XYZW pixel. Selecting [1, 2, 3, 0] produces
// YZWX, and offsets 4, 8, and 12 repeat that rotation for the next pixels.
// The surrounding pipeline subsequently removes every fourth byte.
=> Vector128.ShuffleNative(source, Vector128.Create((byte)1, 2, 3, 0, 5, 6, 7, 4, 9, 10, 11, 8, 13, 14, 15, 12));
}
internal readonly struct XYZWShuffle4Slice3 : IShuffle4Slice3
/// <summary>
/// Reorders XYZW components to WZY before discarding X.
/// </summary>
internal readonly struct WZYXShuffle4Slice3 : IShuffle4Slice3
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination)
=> HwIntrinsics.Shuffle4Slice3Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle3210);
public static uint Invoke(uint source)
// Reuse the four-component reversal; the caller stores only the low WZY
// bytes and therefore discards the reversed X byte.
=> WZYXShuffle4.Invoke(source);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
// Each four-byte group is an XYZW pixel. Selecting [3, 2, 1, 0] produces
// WZYX, and offsets 4, 8, and 12 repeat that reversal for the next pixels.
// The surrounding pipeline subsequently removes every fourth byte.
=> Vector128.ShuffleNative(source, Vector128.Create((byte)3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8, 15, 14, 13, 12));
}
/// <summary>
/// Reorders XYZW components to ZYX before discarding W.
/// </summary>
internal readonly struct ZYXWShuffle4Slice3 : IShuffle4Slice3
{
/// <inheritdoc />
[MethodImpl(InliningOptions.ShortMethod)]
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination)
{
ref uint sBase = ref Unsafe.As<byte, uint>(ref MemoryMarshal.GetReference(source));
ref Byte3 dBase = ref Unsafe.As<byte, Byte3>(ref MemoryMarshal.GetReference(destination));
nint n = (nint)(uint)source.Length / 4;
nint m = Numerics.Modulo4(n);
nint u = n - m;
ref uint sLoopEnd = ref Unsafe.Add(ref sBase, u);
ref uint sEnd = ref Unsafe.Add(ref sBase, n);
while (Unsafe.IsAddressLessThan(ref sBase, ref sLoopEnd))
{
Unsafe.Add(ref dBase, 0) = Unsafe.As<uint, Byte3>(ref Unsafe.Add(ref sBase, 0));
Unsafe.Add(ref dBase, 1) = Unsafe.As<uint, Byte3>(ref Unsafe.Add(ref sBase, 1));
Unsafe.Add(ref dBase, 2) = Unsafe.As<uint, Byte3>(ref Unsafe.Add(ref sBase, 2));
Unsafe.Add(ref dBase, 3) = Unsafe.As<uint, Byte3>(ref Unsafe.Add(ref sBase, 3));
sBase = ref Unsafe.Add(ref sBase, 4);
dBase = ref Unsafe.Add(ref dBase, 4);
}
while (Unsafe.IsAddressLessThan(ref sBase, ref sEnd))
{
Unsafe.Add(ref dBase, 0) = Unsafe.As<uint, Byte3>(ref Unsafe.Add(ref sBase, 0));
sBase = ref Unsafe.Add(ref sBase, 1);
dBase = ref Unsafe.Add(ref dBase, 1);
}
}
public static uint Invoke(uint source)
// Reuse the four-component exchange; the caller stores only the low ZYX
// bytes and therefore discards the preserved W byte.
=> ZYXWShuffle4.Invoke(source);
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> Invoke(Vector128<byte> source)
// Each four-byte group is an XYZW pixel. Selecting [2, 1, 0, 3] produces
// ZYXW, and offsets 4, 8, and 12 repeat that exchange for the next pixels.
// The surrounding pipeline subsequently removes every fourth byte.
=> Vector128.ShuffleNative(source, Vector128.Create((byte)2, 1, 0, 3, 6, 5, 4, 7, 10, 9, 8, 11, 14, 13, 12, 15));
}
/// <summary>
/// Represents one tightly packed three-byte value for scalar four-to-three component writes.
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = 3)]
internal readonly struct Byte3
{

31
src/ImageSharp/Common/Helpers/SimdUtils.Convert.cs

@ -41,11 +41,29 @@ internal static partial class SimdUtils
{
DebugGuard.IsTrue(source.Length == destination.Length, nameof(source), "Input spans must be of same length!");
HwIntrinsics.NormalizedFloatToByteSaturateReduce(ref source, ref destination);
HwIntrinsics.FloatToByteSaturateReduce(ref source, ref destination, byte.MaxValue);
if (source.Length > 0)
{
ConvertNormalizedFloatToByteRemainder(source, destination);
ConvertFloatToByteRemainder(source, destination, byte.MaxValue);
}
}
/// <summary>
/// Converts byte-magnitude floating-point values to bytes using saturating round-to-nearest with midpoint values away from zero.
/// </summary>
/// <param name="source">The source byte magnitudes.</param>
/// <param name="destination">The destination bytes.</param>
[MethodImpl(InliningOptions.ShortMethod)]
internal static void FloatToByteSaturate(ReadOnlySpan<float> source, Span<byte> destination)
{
DebugGuard.IsTrue(source.Length == destination.Length, nameof(source), "Input spans must be of same length!");
HwIntrinsics.FloatToByteSaturateReduce(ref source, ref destination, 1F);
if (source.Length > 0)
{
ConvertFloatToByteRemainder(source, destination, 1F);
}
}
@ -57,22 +75,23 @@ internal static partial class SimdUtils
for (int i = 0; i < source.Length; i++)
{
Unsafe.Add(ref dBase, (uint)i) = Unsafe.Add(ref sBase, (uint)i) / 255f;
// Match the SIMD conversion so one span cannot contain different float representations of the same byte value.
Unsafe.Add(ref dBase, (uint)i) = Unsafe.Add(ref sBase, (uint)i) / (float)byte.MaxValue;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ConvertNormalizedFloatToByteRemainder(ReadOnlySpan<float> source, Span<byte> destination)
private static void ConvertFloatToByteRemainder(ReadOnlySpan<float> source, Span<byte> destination, float scale)
{
ref float sBase = ref MemoryMarshal.GetReference(source);
ref byte dBase = ref MemoryMarshal.GetReference(destination);
for (int i = 0; i < source.Length; i++)
{
Unsafe.Add(ref dBase, (uint)i) = ConvertToByte(Unsafe.Add(ref sBase, (uint)i));
Unsafe.Add(ref dBase, (uint)i) = ConvertToByte(Unsafe.Add(ref sBase, (uint)i), scale);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static byte ConvertToByte(float f) => (byte)Numerics.Clamp((f * 255f) + 0.5f, 0, 255f);
private static byte ConvertToByte(float value, float scale) => (byte)Numerics.Clamp((value * scale) + .5F, 0, byte.MaxValue);
}

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

@ -359,17 +359,17 @@ internal static partial class SimdUtils
ref Vector512<byte> vs0 = ref Unsafe.Add(ref sourceBase, i);
ref Vector512<byte> vd0 = ref Unsafe.Add(ref destinationBase, i);
vd0 = Vector512_.ShuffleNative(vs0, mask);
Unsafe.Add(ref vd0, (nuint)1) = Vector512_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)1), mask);
Unsafe.Add(ref vd0, (nuint)2) = Vector512_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)2), mask);
Unsafe.Add(ref vd0, (nuint)3) = Vector512_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)3), mask);
vd0 = Vector512.ShuffleNative(vs0, mask);
Unsafe.Add(ref vd0, (nuint)1) = Vector512.ShuffleNative(Unsafe.Add(ref vs0, (nuint)1), mask);
Unsafe.Add(ref vd0, (nuint)2) = Vector512.ShuffleNative(Unsafe.Add(ref vs0, (nuint)2), mask);
Unsafe.Add(ref vd0, (nuint)3) = Vector512.ShuffleNative(Unsafe.Add(ref vs0, (nuint)3), mask);
}
if (m > 0)
{
for (nuint i = u; i < n; i++)
{
Unsafe.Add(ref destinationBase, i) = Vector512_.ShuffleNative(Unsafe.Add(ref sourceBase, i), mask);
Unsafe.Add(ref destinationBase, i) = Vector512.ShuffleNative(Unsafe.Add(ref sourceBase, i), mask);
}
}
}
@ -428,17 +428,17 @@ internal static partial class SimdUtils
ref Vector128<byte> vs0 = ref Unsafe.Add(ref sourceBase, i);
ref Vector128<byte> vd0 = ref Unsafe.Add(ref destinationBase, i);
vd0 = Vector128_.ShuffleNative(vs0, mask);
Unsafe.Add(ref vd0, (nuint)1) = Vector128_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)1), mask);
Unsafe.Add(ref vd0, (nuint)2) = Vector128_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)2), mask);
Unsafe.Add(ref vd0, (nuint)3) = Vector128_.ShuffleNative(Unsafe.Add(ref vs0, (nuint)3), mask);
vd0 = Vector128.ShuffleNative(vs0, mask);
Unsafe.Add(ref vd0, (nuint)1) = Vector128.ShuffleNative(Unsafe.Add(ref vs0, (nuint)1), mask);
Unsafe.Add(ref vd0, (nuint)2) = Vector128.ShuffleNative(Unsafe.Add(ref vs0, (nuint)2), mask);
Unsafe.Add(ref vd0, (nuint)3) = Vector128.ShuffleNative(Unsafe.Add(ref vs0, (nuint)3), mask);
}
if (m > 0)
{
for (nuint i = u; i < n; i++)
{
Unsafe.Add(ref destinationBase, i) = Vector128_.ShuffleNative(Unsafe.Add(ref sourceBase, i), mask);
Unsafe.Add(ref destinationBase, i) = Vector128.ShuffleNative(Unsafe.Add(ref sourceBase, i), mask);
}
}
}
@ -477,15 +477,15 @@ internal static partial class SimdUtils
v2 = Vector128_.AlignRight(v2, v1, 8);
v1 = Vector128_.AlignRight(v1, v0, 12);
v0 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v0, maskPad4Nx16), mask);
v1 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v1, maskPad4Nx16), mask);
v2 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v2, maskPad4Nx16), mask);
v3 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v3, maskPad4Nx16), mask);
v0 = Vector128.ShuffleNative(Vector128.ShuffleNative(v0, maskPad4Nx16), mask);
v1 = Vector128.ShuffleNative(Vector128.ShuffleNative(v1, maskPad4Nx16), mask);
v2 = Vector128.ShuffleNative(Vector128.ShuffleNative(v2, maskPad4Nx16), mask);
v3 = Vector128.ShuffleNative(Vector128.ShuffleNative(v3, maskPad4Nx16), mask);
v0 = Vector128_.ShuffleNative(v0, maskE);
v1 = Vector128_.ShuffleNative(v1, maskSlice4Nx16);
v2 = Vector128_.ShuffleNative(v2, maskE);
v3 = Vector128_.ShuffleNative(v3, maskSlice4Nx16);
v0 = Vector128.ShuffleNative(v0, maskE);
v1 = Vector128.ShuffleNative(v1, maskSlice4Nx16);
v2 = Vector128.ShuffleNative(v2, maskE);
v3 = Vector128.ShuffleNative(v3, maskSlice4Nx16);
v0 = Vector128_.AlignRight(v1, v0, 4);
v3 = Vector128_.AlignRight(v3, v2, 12);
@ -539,10 +539,10 @@ internal static partial class SimdUtils
ref Vector128<byte> vd = ref Unsafe.Add(ref destinationBase, j);
vd = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v0, maskPad4Nx16) | fill, mask);
Unsafe.Add(ref vd, 1) = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v1, maskPad4Nx16) | fill, mask);
Unsafe.Add(ref vd, 2) = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v2, maskPad4Nx16) | fill, mask);
Unsafe.Add(ref vd, 3) = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v3, maskPad4Nx16) | fill, mask);
vd = Vector128.ShuffleNative(Vector128.ShuffleNative(v0, maskPad4Nx16) | fill, mask);
Unsafe.Add(ref vd, 1) = Vector128.ShuffleNative(Vector128.ShuffleNative(v1, maskPad4Nx16) | fill, mask);
Unsafe.Add(ref vd, 2) = Vector128.ShuffleNative(Vector128.ShuffleNative(v2, maskPad4Nx16) | fill, mask);
Unsafe.Add(ref vd, 3) = Vector128.ShuffleNative(Vector128.ShuffleNative(v3, maskPad4Nx16) | fill, mask);
}
}
}
@ -579,10 +579,10 @@ internal static partial class SimdUtils
Vector128<byte> v2 = Unsafe.Add(ref vs, 2);
Vector128<byte> v3 = Unsafe.Add(ref vs, 3);
v0 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v0, mask), maskE);
v1 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v1, mask), maskSlice4Nx16);
v2 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v2, mask), maskE);
v3 = Vector128_.ShuffleNative(Vector128_.ShuffleNative(v3, mask), maskSlice4Nx16);
v0 = Vector128.ShuffleNative(Vector128.ShuffleNative(v0, mask), maskE);
v1 = Vector128.ShuffleNative(Vector128.ShuffleNative(v1, mask), maskSlice4Nx16);
v2 = Vector128.ShuffleNative(Vector128.ShuffleNative(v2, mask), maskE);
v3 = Vector128.ShuffleNative(Vector128.ShuffleNative(v3, mask), maskSlice4Nx16);
v0 = Vector128_.AlignRight(v1, v0, 4);
v3 = Vector128_.AlignRight(v3, v2, 12);
@ -601,51 +601,6 @@ internal static partial class SimdUtils
}
}
/// <summary>
/// Performs a multiplication and an addition of the <see cref="Vector256{Single}"/>.
/// TODO: Fix. The arguments are in a different order to the FMA intrinsic.
/// </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>
/// <returns>The <see cref="Vector256{T}"/>.</returns>
[MethodImpl(InliningOptions.AlwaysInline)]
public static Vector256<float> MultiplyAdd(
Vector256<float> va,
Vector256<float> vm0,
Vector256<float> vm1)
{
if (Fma.IsSupported)
{
return Fma.MultiplyAdd(vm1, vm0, va);
}
return va + (vm0 * vm1);
}
/// <summary>
/// Performs a multiplication and a negated addition of the <see cref="Vector256{Single}"/>.
/// </summary>
/// <remarks>ret = c - (a * b)</remarks>
/// <param name="a">The first vector to multiply.</param>
/// <param name="b">The second vector to multiply.</param>
/// <param name="c">The vector to add negated to the intermediate result.</param>
/// <returns>The <see cref="Vector256{T}"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static Vector256<float> MultiplyAddNegated(
Vector256<float> a,
Vector256<float> b,
Vector256<float> c)
{
if (Fma.IsSupported)
{
return Fma.MultiplyAddNegated(a, b, c);
}
return Avx.Subtract(c, Avx.Multiply(a, b));
}
/// <summary>
/// Blend packed 8-bit integers from <paramref name="left"/> and <paramref name="right"/> using <paramref name="mask"/>.
/// The high bit of each corresponding <paramref name="mask"/> byte determines the selection.
@ -752,10 +707,14 @@ internal static partial class SimdUtils
/// Implementation is based on MagicScaler code:
/// https://github.com/saucecontrol/PhotoSauce/blob/b5811908041200488aa18fdfd17df5fc457415dc/src/MagicScaler/Magic/Processors/ConvertersFloat.cs#L80-L182
/// </remarks>
internal static unsafe void ByteToNormalizedFloat(
internal static void ByteToNormalizedFloat(
ReadOnlySpan<byte> source,
Span<float> destination)
{
const double reciprocal = 1D / byte.MaxValue;
const float reciprocalHigh = (float)reciprocal;
const float reciprocalLow = (float)(reciprocal - reciprocalHigh);
if (Vector512.IsHardwareAccelerated && Avx512F.IsSupported)
{
DebugVerifySpanInput(source, destination, Vector512<byte>.Count);
@ -764,6 +723,8 @@ internal static partial class SimdUtils
ref byte sourceBase = ref MemoryMarshal.GetReference(source);
ref Vector512<float> destinationBase = ref Unsafe.As<float, Vector512<float>>(ref MemoryMarshal.GetReference(destination));
Vector512<float> high = Vector512.Create(reciprocalHigh);
Vector512<float> low = Vector512.Create(reciprocalLow);
for (nuint i = 0; i < n; i++)
{
@ -773,11 +734,16 @@ internal static partial class SimdUtils
Vector512<int> i2 = Avx512F.ConvertToVector512Int32(Vector128.LoadUnsafe(ref sourceBase, si + (nuint)(Vector512<int>.Count * 2)));
Vector512<int> i3 = Avx512F.ConvertToVector512Int32(Vector128.LoadUnsafe(ref sourceBase, si + (nuint)(Vector512<int>.Count * 3)));
// Declare multiplier on each line. Codegen is better.
Vector512<float> f0 = Vector512.Create(1 / (float)byte.MaxValue) * Avx512F.ConvertToVector512Single(i0);
Vector512<float> f1 = Vector512.Create(1 / (float)byte.MaxValue) * Avx512F.ConvertToVector512Single(i1);
Vector512<float> f2 = Vector512.Create(1 / (float)byte.MaxValue) * Avx512F.ConvertToVector512Single(i2);
Vector512<float> f3 = Vector512.Create(1 / (float)byte.MaxValue) * Avx512F.ConvertToVector512Single(i3);
Vector512<float> f0 = Avx512F.ConvertToVector512Single(i0);
Vector512<float> f1 = Avx512F.ConvertToVector512Single(i1);
Vector512<float> f2 = Avx512F.ConvertToVector512Single(i2);
Vector512<float> f3 = Avx512F.ConvertToVector512Single(i3);
// The residual term restores the correctly rounded byte / 255F result without paying for vector division.
f0 = Vector512.FusedMultiplyAdd(f0, high, f0 * low);
f1 = Vector512.FusedMultiplyAdd(f1, high, f1 * low);
f2 = Vector512.FusedMultiplyAdd(f2, high, f2 * low);
f3 = Vector512.FusedMultiplyAdd(f3, high, f3 * low);
ref Vector512<float> d = ref Unsafe.Add(ref destinationBase, i * 4);
@ -795,6 +761,8 @@ internal static partial class SimdUtils
ref byte sourceBase = ref MemoryMarshal.GetReference(source);
ref Vector256<float> destinationBase = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(destination));
Vector256<float> high = Vector256.Create(reciprocalHigh);
Vector256<float> low = Vector256.Create(reciprocalLow);
for (nuint i = 0; i < n; i++)
{
@ -807,11 +775,15 @@ internal static partial class SimdUtils
ref ulong refULong = ref Unsafe.As<byte, ulong>(ref Unsafe.Add(ref sourceBase, si));
Vector256<int> i3 = Avx2.ConvertToVector256Int32(Vector128.CreateScalarUnsafe(Unsafe.Add(ref refULong, 3)).AsByte());
// Declare multiplier on each line. Codegen is better.
Vector256<float> f0 = Vector256.Create(1 / (float)byte.MaxValue) * Avx.ConvertToVector256Single(i0);
Vector256<float> f1 = Vector256.Create(1 / (float)byte.MaxValue) * Avx.ConvertToVector256Single(i1);
Vector256<float> f2 = Vector256.Create(1 / (float)byte.MaxValue) * Avx.ConvertToVector256Single(i2);
Vector256<float> f3 = Vector256.Create(1 / (float)byte.MaxValue) * Avx.ConvertToVector256Single(i3);
Vector256<float> f0 = Avx.ConvertToVector256Single(i0);
Vector256<float> f1 = Avx.ConvertToVector256Single(i1);
Vector256<float> f2 = Avx.ConvertToVector256Single(i2);
Vector256<float> f3 = Avx.ConvertToVector256Single(i3);
f0 = Vector256.FusedMultiplyAdd(f0, high, f0 * low);
f1 = Vector256.FusedMultiplyAdd(f1, high, f1 * low);
f2 = Vector256.FusedMultiplyAdd(f2, high, f2 * low);
f3 = Vector256.FusedMultiplyAdd(f3, high, f3 * low);
ref Vector256<float> d = ref Unsafe.Add(ref destinationBase, i * 4);
@ -830,7 +802,8 @@ internal static partial class SimdUtils
ref byte sourceBase = ref MemoryMarshal.GetReference(source);
ref Vector128<float> destinationBase = ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(destination));
Vector128<float> scale = Vector128.Create(1 / (float)byte.MaxValue);
Vector128<float> high = Vector128.Create(reciprocalHigh);
Vector128<float> low = Vector128.Create(reciprocalLow);
for (nuint i = 0; i < n; i++)
{
@ -855,10 +828,15 @@ internal static partial class SimdUtils
(i2, i3) = Vector128.Widen(s1.AsInt16());
}
Vector128<float> f0 = scale * Vector128.ConvertToSingle(i0);
Vector128<float> f1 = scale * Vector128.ConvertToSingle(i1);
Vector128<float> f2 = scale * Vector128.ConvertToSingle(i2);
Vector128<float> f3 = scale * Vector128.ConvertToSingle(i3);
Vector128<float> f0 = Vector128.ConvertToSingle(i0);
Vector128<float> f1 = Vector128.ConvertToSingle(i1);
Vector128<float> f2 = Vector128.ConvertToSingle(i2);
Vector128<float> f3 = Vector128.ConvertToSingle(i3);
f0 = Vector128.FusedMultiplyAdd(f0, high, f0 * low);
f1 = Vector128.FusedMultiplyAdd(f1, high, f1 * low);
f2 = Vector128.FusedMultiplyAdd(f2, high, f2 * low);
f3 = Vector128.FusedMultiplyAdd(f3, high, f3 * low);
ref Vector128<float> d = ref Unsafe.Add(ref destinationBase, i * 4);
@ -879,6 +857,19 @@ internal static partial class SimdUtils
internal static void NormalizedFloatToByteSaturateReduce(
ref ReadOnlySpan<float> source,
ref Span<byte> destination)
=> FloatToByteSaturateReduce(ref source, ref destination, byte.MaxValue);
/// <summary>
/// Converts as many scaled floating-point values as possible to bytes and retains the unconverted remainder.
/// </summary>
/// <param name="source">The source buffer.</param>
/// <param name="destination">The destination buffer.</param>
/// <param name="scaleFactor">The factor applied before conversion.</param>
[MethodImpl(InliningOptions.ShortMethod)]
internal static void FloatToByteSaturateReduce(
ref ReadOnlySpan<float> source,
ref Span<byte> destination,
float scaleFactor)
{
DebugGuard.IsTrue(source.Length == destination.Length, nameof(source), "Input spans must be of same length!");
@ -903,9 +894,10 @@ internal static partial class SimdUtils
if (adjustedCount > 0)
{
NormalizedFloatToByteSaturate(
FloatToByteSaturate(
source[..adjustedCount],
destination[..adjustedCount]);
destination[..adjustedCount],
scaleFactor);
source = source[adjustedCount..];
destination = destination[adjustedCount..];
@ -925,6 +917,18 @@ internal static partial class SimdUtils
internal static void NormalizedFloatToByteSaturate(
ReadOnlySpan<float> source,
Span<byte> destination)
=> FloatToByteSaturate(source, destination, byte.MaxValue);
/// <summary>
/// Converts scaled floating-point values to bytes using saturating round-to-nearest with midpoint values away from zero.
/// </summary>
/// <param name="source">The source buffer.</param>
/// <param name="destination">The destination buffer.</param>
/// <param name="scaleFactor">The factor applied before conversion.</param>
internal static void FloatToByteSaturate(
ReadOnlySpan<float> source,
Span<byte> destination,
float scaleFactor)
{
if (Vector512.IsHardwareAccelerated && Avx512BW.IsSupported)
{
@ -935,7 +939,7 @@ internal static partial class SimdUtils
ref Vector512<float> sourceBase = ref Unsafe.As<float, Vector512<float>>(ref MemoryMarshal.GetReference(source));
ref Vector512<byte> destinationBase = ref Unsafe.As<byte, Vector512<byte>>(ref MemoryMarshal.GetReference(destination));
Vector512<float> scale = Vector512.Create((float)byte.MaxValue);
Vector512<float> scale = Vector512.Create(scaleFactor);
Vector512<int> mask = PermuteMaskDeinterleave16x32();
for (nuint i = 0; i < n; i++)
@ -947,10 +951,10 @@ internal static partial class SimdUtils
Vector512<float> f2 = scale * Unsafe.Add(ref s, 2);
Vector512<float> f3 = scale * Unsafe.Add(ref s, 3);
Vector512<int> w0 = Vector512_.ConvertToInt32RoundToEven(f0);
Vector512<int> w1 = Vector512_.ConvertToInt32RoundToEven(f1);
Vector512<int> w2 = Vector512_.ConvertToInt32RoundToEven(f2);
Vector512<int> w3 = Vector512_.ConvertToInt32RoundToEven(f3);
Vector512<int> w0 = Vector512_.ConvertToInt32RoundAwayFromZero(f0);
Vector512<int> w1 = Vector512_.ConvertToInt32RoundAwayFromZero(f1);
Vector512<int> w2 = Vector512_.ConvertToInt32RoundAwayFromZero(f2);
Vector512<int> w3 = Vector512_.ConvertToInt32RoundAwayFromZero(f3);
Vector512<short> u0 = Avx512BW.PackSignedSaturate(w0, w1);
Vector512<short> u1 = Avx512BW.PackSignedSaturate(w2, w3);
@ -969,7 +973,7 @@ internal static partial class SimdUtils
ref Vector256<float> sourceBase = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(source));
ref Vector256<byte> destinationBase = ref Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(destination));
Vector256<float> scale = Vector256.Create((float)byte.MaxValue);
Vector256<float> scale = Vector256.Create(scaleFactor);
Vector256<int> mask = PermuteMaskDeinterleave8x32();
for (nuint i = 0; i < n; i++)
@ -981,10 +985,10 @@ internal static partial class SimdUtils
Vector256<float> f2 = scale * Unsafe.Add(ref s, 2);
Vector256<float> f3 = scale * Unsafe.Add(ref s, 3);
Vector256<int> w0 = Vector256_.ConvertToInt32RoundToEven(f0);
Vector256<int> w1 = Vector256_.ConvertToInt32RoundToEven(f1);
Vector256<int> w2 = Vector256_.ConvertToInt32RoundToEven(f2);
Vector256<int> w3 = Vector256_.ConvertToInt32RoundToEven(f3);
Vector256<int> w0 = Vector256_.ConvertToInt32RoundAwayFromZero(f0);
Vector256<int> w1 = Vector256_.ConvertToInt32RoundAwayFromZero(f1);
Vector256<int> w2 = Vector256_.ConvertToInt32RoundAwayFromZero(f2);
Vector256<int> w3 = Vector256_.ConvertToInt32RoundAwayFromZero(f3);
Vector256<short> u0 = Avx2.PackSignedSaturate(w0, w1);
Vector256<short> u1 = Avx2.PackSignedSaturate(w2, w3);
@ -1004,7 +1008,7 @@ internal static partial class SimdUtils
ref Vector128<float> sourceBase = ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(source));
ref Vector128<byte> destinationBase = ref Unsafe.As<byte, Vector128<byte>>(ref MemoryMarshal.GetReference(destination));
Vector128<float> scale = Vector128.Create((float)byte.MaxValue);
Vector128<float> scale = Vector128.Create(scaleFactor);
Vector128<int> min = Vector128<int>.Zero;
Vector128<int> max = Vector128.Create((int)byte.MaxValue);
@ -1017,15 +1021,15 @@ internal static partial class SimdUtils
Vector128<float> f2 = scale * Unsafe.Add(ref s, 2);
Vector128<float> f3 = scale * Unsafe.Add(ref s, 3);
Vector128<int> w0 = Vector128_.ConvertToInt32RoundToEven(f0);
Vector128<int> w1 = Vector128_.ConvertToInt32RoundToEven(f1);
Vector128<int> w2 = Vector128_.ConvertToInt32RoundToEven(f2);
Vector128<int> w3 = Vector128_.ConvertToInt32RoundToEven(f3);
Vector128<int> w0 = Vector128_.ConvertToInt32RoundAwayFromZero(f0);
Vector128<int> w1 = Vector128_.ConvertToInt32RoundAwayFromZero(f1);
Vector128<int> w2 = Vector128_.ConvertToInt32RoundAwayFromZero(f2);
Vector128<int> w3 = Vector128_.ConvertToInt32RoundAwayFromZero(f3);
w0 = Vector128_.Clamp(w0, min, max);
w1 = Vector128_.Clamp(w1, min, max);
w2 = Vector128_.Clamp(w2, min, max);
w3 = Vector128_.Clamp(w3, min, max);
w0 = Vector128.Clamp(w0, min, max);
w1 = Vector128.Clamp(w1, min, max);
w2 = Vector128.Clamp(w2, min, max);
w3 = Vector128.Clamp(w3, min, max);
Vector128<ushort> u0 = Vector128.Narrow(w0, w1).AsUInt16();
Vector128<ushort> u1 = Vector128.Narrow(w2, w3).AsUInt16();
@ -1172,8 +1176,10 @@ internal static partial class SimdUtils
Vector256<byte> rgb, rg, bx;
Vector256<float> r, g, b;
// Each iteration consumes 8 Rgb24 pixels (24 bytes) but starts with a 32-byte load,
// so we need 3 extra pixels of addressable slack beyond the vectorized chunk.
const int bytesPerRgbStride = 24;
nuint count = (uint)source.Length / 8;
nuint count = source.Length > 3 ? (uint)(source.Length - 3) / 8 : 0;
for (nuint i = 0; i < count; i++)
{
rgb = Avx2.PermuteVar8x32(Unsafe.AddByteOffset(ref rgbByteSpan, (uint)(bytesPerRgbStride * i)).AsUInt32(), extractToLanesMask).AsByte();
@ -1193,10 +1199,10 @@ internal static partial class SimdUtils
}
int sliceCount = (int)(count * 8);
redChannel = redChannel.Slice(sliceCount);
greenChannel = greenChannel.Slice(sliceCount);
blueChannel = blueChannel.Slice(sliceCount);
source = source.Slice(sliceCount);
redChannel = redChannel[sliceCount..];
greenChannel = greenChannel[sliceCount..];
blueChannel = blueChannel[sliceCount..];
source = source[sliceCount..];
}
}
}

378
src/ImageSharp/Common/Helpers/SimdUtils.Shuffle.cs

@ -1,10 +1,13 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp;
@ -41,22 +44,89 @@ internal static partial class SimdUtils
/// <typeparam name="TShuffle">The type of shuffle struct.</typeparam>
/// <param name="source">The source span of bytes.</param>
/// <param name="destination">The destination span of bytes.</param>
/// <param name="shuffle">The type of shuffle to perform.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static void Shuffle4<TShuffle>(
ReadOnlySpan<byte> source,
Span<byte> destination,
TShuffle shuffle)
Span<byte> destination)
where TShuffle : struct, IShuffle4
{
VerifyShuffle4SpanInput(source, destination);
shuffle.ShuffleReduce(ref source, ref destination);
ref byte sourceBase = ref MemoryMarshal.GetReference(source);
ref byte destinationBase = ref MemoryMarshal.GetReference(destination);
int length = source.Length;
int i = 0;
// Deal with the remainder:
if (source.Length > 0)
// The same offset flows through descending widths. This keeps a single traversal while
// allowing a row that is not a multiple of the widest register to retain a vectorized tail.
if (Vector512.IsHardwareAccelerated)
{
int fourVectorsFromEnd = length - (Vector512<byte>.Count * 4);
for (; i <= fourVectorsFromEnd; i += Vector512<byte>.Count * 4)
{
// Four independent vectors amortize loop control and expose enough work for the CPU
// to overlap loads, byte shuffles, and stores without changing pixel ordering.
TShuffle.Invoke(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)).StoreUnsafe(ref destinationBase, (nuint)i);
TShuffle.Invoke(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512<byte>.Count))).StoreUnsafe(ref destinationBase, (nuint)(i + Vector512<byte>.Count));
TShuffle.Invoke(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + (Vector512<byte>.Count * 2)))).StoreUnsafe(ref destinationBase, (nuint)(i + (Vector512<byte>.Count * 2)));
TShuffle.Invoke(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + (Vector512<byte>.Count * 3)))).StoreUnsafe(ref destinationBase, (nuint)(i + (Vector512<byte>.Count * 3)));
}
int oneVectorFromEnd = length - Vector512<byte>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<byte>.Count)
{
TShuffle.Invoke(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)).StoreUnsafe(ref destinationBase, (nuint)i);
}
}
if (Vector256.IsHardwareAccelerated)
{
int fourVectorsFromEnd = length - (Vector256<byte>.Count * 4);
for (; i <= fourVectorsFromEnd; i += Vector256<byte>.Count * 4)
{
TShuffle.Invoke(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)).StoreUnsafe(ref destinationBase, (nuint)i);
TShuffle.Invoke(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256<byte>.Count))).StoreUnsafe(ref destinationBase, (nuint)(i + Vector256<byte>.Count));
TShuffle.Invoke(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + (Vector256<byte>.Count * 2)))).StoreUnsafe(ref destinationBase, (nuint)(i + (Vector256<byte>.Count * 2)));
TShuffle.Invoke(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + (Vector256<byte>.Count * 3)))).StoreUnsafe(ref destinationBase, (nuint)(i + (Vector256<byte>.Count * 3)));
}
int oneVectorFromEnd = length - Vector256<byte>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<byte>.Count)
{
TShuffle.Invoke(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)).StoreUnsafe(ref destinationBase, (nuint)i);
}
}
if (Vector128.IsHardwareAccelerated)
{
shuffle.Shuffle(source, destination);
int fourVectorsFromEnd = length - (Vector128<byte>.Count * 4);
for (; i <= fourVectorsFromEnd; i += Vector128<byte>.Count * 4)
{
TShuffle.Invoke(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)).StoreUnsafe(ref destinationBase, (nuint)i);
TShuffle.Invoke(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128<byte>.Count))).StoreUnsafe(ref destinationBase, (nuint)(i + Vector128<byte>.Count));
TShuffle.Invoke(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + (Vector128<byte>.Count * 2)))).StoreUnsafe(ref destinationBase, (nuint)(i + (Vector128<byte>.Count * 2)));
TShuffle.Invoke(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + (Vector128<byte>.Count * 3)))).StoreUnsafe(ref destinationBase, (nuint)(i + (Vector128<byte>.Count * 3)));
}
int oneVectorFromEnd = length - Vector128<byte>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<byte>.Count)
{
TShuffle.Invoke(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)).StoreUnsafe(ref destinationBase, (nuint)i);
}
}
// The vector cascade leaves fewer than four pixels. A full uint load keeps each pixel
// in a register while the closed operator resolves to its rotate, reverse, or mask sequence.
for (; i < length; i += 4)
{
uint packed = Unsafe.As<byte, uint>(ref Unsafe.Add(ref sourceBase, (nuint)i));
Unsafe.As<byte, uint>(ref Unsafe.Add(ref destinationBase, (nuint)i)) = TShuffle.Invoke(packed);
}
}
@ -67,23 +137,114 @@ internal static partial class SimdUtils
/// <typeparam name="TShuffle">The type of shuffle struct.</typeparam>
/// <param name="source">The source span of bytes.</param>
/// <param name="destination">The destination span of bytes.</param>
/// <param name="shuffle">The type of shuffle to perform.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static void Shuffle3<TShuffle>(
ReadOnlySpan<byte> source,
Span<byte> destination,
TShuffle shuffle)
Span<byte> destination)
where TShuffle : struct, IShuffle3
{
// Source length should be smaller than destination length, and divisible by 3.
VerifyShuffle3SpanInput(source, destination);
shuffle.ShuffleReduce(ref source, ref destination);
ref byte sourceBase = ref MemoryMarshal.GetReference(source);
ref byte destinationBase = ref MemoryMarshal.GetReference(destination);
int length = source.Length;
int i = 0;
// Deal with the remainder:
if (source.Length > 0)
if (Vector128.IsHardwareAccelerated)
{
// Each group contains sixteen XYZ pixels in three registers. For a register beginning
// [X0,Y0,Z0,X1,Y1,Z1,...], the indices [0,1,2,0x80,3,4,5,0x80,...]
// produce four [X,Y,Z,0] pixels. Index 0x80 selects zero on the native byte-shuffle
// instructions and on the portable helper, creating a temporary W lane for TShuffle.
Vector128<byte> padMask = Vector128.Create((byte)0, 1, 2, 0x80, 3, 4, 5, 0x80, 6, 7, 8, 0x80, 9, 10, 11, 0x80);
// After TShuffle places the retained components in bytes 0..2 of each four-byte pixel,
// [0,1,2,4,5,6,8,9,10,12,13,14] packs four triplets into twelve bytes. Rotating that
// mask by twelve positions moves the packed bytes four positions right. Alternating the
// two alignments lets AlignRight stitch four twelve-byte results into three full registers.
Vector128<byte> sliceMask = Vector128.Create((byte)0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 0x80, 0x80, 0x80, 0x80);
Vector128<byte> sliceEndMask = Vector128_.AlignRight(sliceMask, sliceMask, 12);
ref Vector128<byte> sourceVectors = ref Unsafe.As<byte, Vector128<byte>>(ref sourceBase);
ref Vector128<byte> destinationVectors = ref Unsafe.As<byte, Vector128<byte>>(ref destinationBase);
nuint sourceVectorCount = (uint)length / (uint)Vector128<byte>.Count;
nuint vectorIndex = 0;
for (; vectorIndex + 2 < sourceVectorCount; vectorIndex += 3)
{
// Realign the three source registers into four registers holding four complete
// triplets apiece. All source registers are captured before any destination store.
ref Vector128<byte> source0 = ref Unsafe.Add(ref sourceVectors, vectorIndex);
Vector128<byte> v0 = source0;
Vector128<byte> v1 = Unsafe.Add(ref source0, 1);
Vector128<byte> v2 = Unsafe.Add(ref source0, 2);
Vector128<byte> v3 = Vector128_.ShiftRightBytesInVector(v2, 4);
v2 = Vector128_.AlignRight(v2, v1, 8);
v1 = Vector128_.AlignRight(v1, v0, 12);
v0 = TShuffle.Invoke(Vector128.ShuffleNative(v0, padMask));
v1 = TShuffle.Invoke(Vector128.ShuffleNative(v1, padMask));
v2 = TShuffle.Invoke(Vector128.ShuffleNative(v2, padMask));
v3 = TShuffle.Invoke(Vector128.ShuffleNative(v3, padMask));
v0 = Vector128.ShuffleNative(v0, sliceEndMask);
v1 = Vector128.ShuffleNative(v1, sliceMask);
v2 = Vector128.ShuffleNative(v2, sliceEndMask);
v3 = Vector128.ShuffleNative(v3, sliceMask);
Vector128<byte> destination0 = Vector128_.AlignRight(v1, v0, 4);
Vector128<byte> destination2 = Vector128_.AlignRight(v3, v2, 12);
v1 = Vector128_.ShiftLeftBytesInVector(v1, 4);
v2 = Vector128_.ShiftRightBytesInVector(v2, 4);
Vector128<byte> destination1 = Vector128_.AlignRight(v2, v1, 8);
ref Vector128<byte> destination0Ref = ref Unsafe.Add(ref destinationVectors, vectorIndex);
destination0Ref = destination0;
Unsafe.Add(ref destination0Ref, 1) = destination1;
Unsafe.Add(ref destination0Ref, 2) = destination2;
}
i = (int)(vectorIndex * (uint)Vector128<byte>.Count);
int oneTailVectorFromEnd = length - Vector128<byte>.Count;
for (; i <= oneTailVectorFromEnd; i += 12)
{
// A single readable register contains four complete triplets plus four bytes from
// the following pixels. The pad mask ignores those extra bytes before the operator
// runs, and the slice mask packs the four results into the low twelve bytes.
Vector128<byte> result = Vector128.LoadUnsafe(ref sourceBase, (nuint)i);
result = Vector128.ShuffleNative(result, padMask);
result = TShuffle.Invoke(result);
result = Vector128.ShuffleNative(result, sliceMask);
// Store exactly twelve bytes so an in-place shuffle does not overwrite the next
// source triplet captured by the following iteration.
Unsafe.As<byte, Vector64<byte>>(ref Unsafe.Add(ref destinationBase, (nuint)i)) = result.GetLower();
Unsafe.As<byte, uint>(ref Unsafe.Add(ref destinationBase, (nuint)(i + 8))) = result.AsUInt32().GetElement(2);
}
}
int widenedReadEnd = length - 3;
for (; i < widenedReadEnd; i += 3)
{
shuffle.Shuffle(source, destination);
// The fourth byte belongs to the following pixel, but the operator only contributes the
// low three result bytes. This unaligned read replaces three dependent byte loads safely.
uint packed = Unsafe.As<byte, uint>(ref Unsafe.Add(ref sourceBase, (nuint)i));
uint shuffled = TShuffle.Invoke(packed);
Unsafe.As<byte, Byte3>(ref Unsafe.Add(ref destinationBase, (nuint)i)) = Unsafe.As<uint, Byte3>(ref shuffled);
}
if (i < length)
{
// The final triplet has no fourth readable byte, so construct only this terminal pixel.
uint packed =
Unsafe.Add(ref sourceBase, (nuint)i) |
((uint)Unsafe.Add(ref sourceBase, (nuint)(i + 1)) << 8) |
((uint)Unsafe.Add(ref sourceBase, (nuint)(i + 2)) << 16);
uint shuffled = TShuffle.Invoke(packed);
Unsafe.As<byte, Byte3>(ref Unsafe.Add(ref destinationBase, (nuint)i)) = Unsafe.As<uint, Byte3>(ref shuffled);
}
}
@ -94,22 +255,81 @@ internal static partial class SimdUtils
/// <typeparam name="TShuffle">The type of shuffle struct.</typeparam>
/// <param name="source">The source span of bytes.</param>
/// <param name="destination">The destination span of bytes.</param>
/// <param name="shuffle">The type of shuffle to perform.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static void Pad3Shuffle4<TShuffle>(
ReadOnlySpan<byte> source,
Span<byte> destination,
TShuffle shuffle)
Span<byte> destination)
where TShuffle : struct, IPad3Shuffle4
{
VerifyPad3Shuffle4SpanInput(source, destination);
shuffle.ShuffleReduce(ref source, ref destination);
ref byte sourceBase = ref MemoryMarshal.GetReference(source);
ref byte destinationBase = ref MemoryMarshal.GetReference(destination);
int sourceLength = source.Length;
int sourceOffset = 0;
int destinationOffset = 0;
// Deal with the remainder:
if (source.Length > 0)
if (Vector128.IsHardwareAccelerated)
{
// For source bytes [X0,Y0,Z0,X1,Y1,Z1,...], the indices
// [0,1,2,0x80,3,4,5,0x80,...] form four [X,Y,Z,0] pixels. The native
// and portable shuffle paths both interpret 0x80 as a zero-producing index.
Vector128<byte> padMask = Vector128.Create((byte)0, 1, 2, 0x80, 3, 4, 5, 0x80, 6, 7, 8, 0x80, 9, 10, 11, 0x80);
// The broadcast ulong has 0xFF in bytes 3 and 7; repeating it across 128 bits
// fills W at byte positions 3, 7, 11, and 15 without modifying X, Y, or Z.
Vector128<byte> opaqueAlpha = Vector128.Create(0xFF000000FF000000UL).AsByte();
ref Vector128<byte> sourceVectors = ref Unsafe.As<byte, Vector128<byte>>(ref sourceBase);
ref Vector128<byte> destinationVectors = ref Unsafe.As<byte, Vector128<byte>>(ref destinationBase);
nuint sourceVectorCount = (uint)sourceLength / (uint)Vector128<byte>.Count;
nuint sourceVectorIndex = 0;
nuint destinationVectorIndex = 0;
for (; sourceVectorIndex + 2 < sourceVectorCount;
sourceVectorIndex += 3, destinationVectorIndex += 4)
{
// Three source registers contain sixteen packed triplets. Aligning at 12, 8, and
// 4-byte boundaries produces four registers whose low twelve bytes each hold four pixels.
ref Vector128<byte> source0 = ref Unsafe.Add(ref sourceVectors, sourceVectorIndex);
Vector128<byte> v0 = source0;
Vector128<byte> v1 = Unsafe.Add(ref source0, 1);
Vector128<byte> v2 = Unsafe.Add(ref source0, 2);
Vector128<byte> v3 = Vector128_.ShiftRightBytesInVector(v2, 4);
v2 = Vector128_.AlignRight(v2, v1, 8);
v1 = Vector128_.AlignRight(v1, v0, 12);
ref Vector128<byte> destination0 = ref Unsafe.Add(ref destinationVectors, destinationVectorIndex);
destination0 = TShuffle.Invoke(Vector128.ShuffleNative(v0, padMask) | opaqueAlpha);
Unsafe.Add(ref destination0, 1) = TShuffle.Invoke(Vector128.ShuffleNative(v1, padMask) | opaqueAlpha);
Unsafe.Add(ref destination0, 2) = TShuffle.Invoke(Vector128.ShuffleNative(v2, padMask) | opaqueAlpha);
Unsafe.Add(ref destination0, 3) = TShuffle.Invoke(Vector128.ShuffleNative(v3, padMask) | opaqueAlpha);
}
sourceOffset = (int)(sourceVectorIndex * (uint)Vector128<byte>.Count);
destinationOffset = (int)(destinationVectorIndex * (uint)Vector128<byte>.Count);
}
int widenedReadEnd = sourceLength - 3;
for (; sourceOffset < widenedReadEnd; sourceOffset += 3, destinationOffset += 4)
{
// The widened load intentionally includes the next pixel's first byte. Replacing that
// high byte with opaque alpha yields the complete XYZW value with one unaligned read.
uint packed = Unsafe.As<byte, uint>(ref Unsafe.Add(ref sourceBase, (nuint)sourceOffset)) | 0xFF000000;
Unsafe.As<byte, uint>(ref Unsafe.Add(ref destinationBase, (nuint)destinationOffset)) = TShuffle.Invoke(packed);
}
if (sourceOffset < sourceLength)
{
shuffle.Shuffle(source, destination);
// The final triplet cannot use the widened load because no following byte is in range.
uint packed =
Unsafe.Add(ref sourceBase, (nuint)sourceOffset) |
((uint)Unsafe.Add(ref sourceBase, (nuint)(sourceOffset + 1)) << 8) |
((uint)Unsafe.Add(ref sourceBase, (nuint)(sourceOffset + 2)) << 16) |
0xFF000000;
Unsafe.As<byte, uint>(ref Unsafe.Add(ref destinationBase, (nuint)destinationOffset)) = TShuffle.Invoke(packed);
}
}
@ -120,22 +340,103 @@ internal static partial class SimdUtils
/// <typeparam name="TShuffle">The type of shuffle struct.</typeparam>
/// <param name="source">The source span of bytes.</param>
/// <param name="destination">The destination span of bytes.</param>
/// <param name="shuffle">The type of shuffle to perform.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static void Shuffle4Slice3<TShuffle>(
ReadOnlySpan<byte> source,
Span<byte> destination,
TShuffle shuffle)
Span<byte> destination)
where TShuffle : struct, IShuffle4Slice3
{
VerifyShuffle4Slice3SpanInput(source, destination);
shuffle.ShuffleReduce(ref source, ref destination);
ref byte sourceBase = ref MemoryMarshal.GetReference(source);
ref byte destinationBase = ref MemoryMarshal.GetReference(destination);
int sourceLength = source.Length;
int sourceOffset = 0;
int destinationOffset = 0;
// Deal with the remainder:
if (source.Length > 0)
if (Vector128.IsHardwareAccelerated)
{
// Each operator first places the retained components in bytes 0..2 of every four-byte
// pixel. The indices [0,1,2,4,5,6,8,9,10,12,13,14] delete each fourth byte and
// pack four triplets into the low twelve bytes. Indices 0x80 zero the unused bytes.
Vector128<byte> sliceMask = Vector128.Create((byte)0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 0x80, 0x80, 0x80, 0x80);
// Rotating the mask by twelve moves its packed result four bytes right. Alternating
// shifted and unshifted results gives AlignRight the overlap needed to concatenate
// four twelve-byte groups into three complete destination registers.
Vector128<byte> sliceEndMask = Vector128_.AlignRight(sliceMask, sliceMask, 12);
ref Vector128<byte> sourceVectors = ref Unsafe.As<byte, Vector128<byte>>(ref sourceBase);
ref Vector128<byte> destinationVectors = ref Unsafe.As<byte, Vector128<byte>>(ref destinationBase);
nuint sourceVectorCount = (uint)sourceLength / (uint)Vector128<byte>.Count;
nuint sourceVectorIndex = 0;
nuint destinationVectorIndex = 0;
for (; sourceVectorIndex + 3 < sourceVectorCount; sourceVectorIndex += 4, destinationVectorIndex += 3)
{
// Load and transform all sixteen source pixels before writing the shorter output group.
// This preserves forward progress when source and destination begin at the same address.
ref Vector128<byte> source0 = ref Unsafe.Add(ref sourceVectors, sourceVectorIndex);
Vector128<byte> v0 = TShuffle.Invoke(source0);
Vector128<byte> v1 = TShuffle.Invoke(Unsafe.Add(ref source0, 1));
Vector128<byte> v2 = TShuffle.Invoke(Unsafe.Add(ref source0, 2));
Vector128<byte> v3 = TShuffle.Invoke(Unsafe.Add(ref source0, 3));
v0 = Vector128.ShuffleNative(v0, sliceEndMask);
v1 = Vector128.ShuffleNative(v1, sliceMask);
v2 = Vector128.ShuffleNative(v2, sliceEndMask);
v3 = Vector128.ShuffleNative(v3, sliceMask);
Vector128<byte> destination0 = Vector128_.AlignRight(v1, v0, 4);
Vector128<byte> destination2 = Vector128_.AlignRight(v3, v2, 12);
v1 = Vector128_.ShiftLeftBytesInVector(v1, 4);
v2 = Vector128_.ShiftRightBytesInVector(v2, 4);
Vector128<byte> destination1 = Vector128_.AlignRight(v2, v1, 8);
ref Vector128<byte> destination0Ref = ref Unsafe.Add(ref destinationVectors, destinationVectorIndex);
destination0Ref = destination0;
Unsafe.Add(ref destination0Ref, 1) = destination1;
Unsafe.Add(ref destination0Ref, 2) = destination2;
}
sourceOffset = (int)(sourceVectorIndex * (uint)Vector128<byte>.Count);
destinationOffset = (int)(destinationVectorIndex * (uint)Vector128<byte>.Count);
int oneTailVectorFromEnd = sourceLength - Vector128<byte>.Count;
for (; sourceOffset <= oneTailVectorFromEnd; sourceOffset += 16, destinationOffset += 12)
{
// The operator arranges the three retained components at the front of each pixel.
// One fixed shuffle then compacts four pixels into the low twelve vector bytes.
Vector128<byte> result = TShuffle.Invoke(Vector128.LoadUnsafe(ref sourceBase, (nuint)sourceOffset));
result = Vector128.ShuffleNative(result, sliceMask);
// The split store writes the exact 12-byte result and remains safe for in-place shrinking.
Unsafe.As<byte, Vector64<byte>>(ref Unsafe.Add(ref destinationBase, (nuint)destinationOffset)) = result.GetLower();
Unsafe.As<byte, uint>(ref Unsafe.Add(ref destinationBase, (nuint)(destinationOffset + 8))) = result.AsUInt32().GetElement(2);
}
}
int fourPixelsFromEnd = sourceLength - 16;
for (; sourceOffset <= fourPixelsFromEnd; sourceOffset += 16, destinationOffset += 12)
{
// Transform four complete pixels before the first three-byte store. Keeping the source
// values in registers avoids reloads after an in-place shrinking destination advances.
uint packed0 = TShuffle.Invoke(Unsafe.As<byte, uint>(ref Unsafe.Add(ref sourceBase, (nuint)sourceOffset)));
uint packed1 = TShuffle.Invoke(Unsafe.As<byte, uint>(ref Unsafe.Add(ref sourceBase, (nuint)(sourceOffset + 4))));
uint packed2 = TShuffle.Invoke(Unsafe.As<byte, uint>(ref Unsafe.Add(ref sourceBase, (nuint)(sourceOffset + 8))));
uint packed3 = TShuffle.Invoke(Unsafe.As<byte, uint>(ref Unsafe.Add(ref sourceBase, (nuint)(sourceOffset + 12))));
Unsafe.As<byte, Byte3>(ref Unsafe.Add(ref destinationBase, (nuint)destinationOffset)) = Unsafe.As<uint, Byte3>(ref packed0);
Unsafe.As<byte, Byte3>(ref Unsafe.Add(ref destinationBase, (nuint)(destinationOffset + 3))) = Unsafe.As<uint, Byte3>(ref packed1);
Unsafe.As<byte, Byte3>(ref Unsafe.Add(ref destinationBase, (nuint)(destinationOffset + 6))) = Unsafe.As<uint, Byte3>(ref packed2);
Unsafe.As<byte, Byte3>(ref Unsafe.Add(ref destinationBase, (nuint)(destinationOffset + 9))) = Unsafe.As<uint, Byte3>(ref packed3);
}
for (; sourceOffset < sourceLength; sourceOffset += 4, destinationOffset += 3)
{
shuffle.Shuffle(source, destination);
uint packed = TShuffle.Invoke(Unsafe.As<byte, uint>(ref Unsafe.Add(ref sourceBase, (nuint)sourceOffset)));
Unsafe.As<byte, Byte3>(ref Unsafe.Add(ref destinationBase, (nuint)destinationOffset)) = Unsafe.As<uint, Byte3>(ref packed);
}
}
@ -150,15 +451,20 @@ internal static partial class SimdUtils
for (nuint i = 0; i < (uint)source.Length; i += 4)
{
Unsafe.Add(ref dBase, i + 0) = Unsafe.Add(ref sBase, p0 + i);
Unsafe.Add(ref dBase, i + 1) = Unsafe.Add(ref sBase, p1 + i);
Unsafe.Add(ref dBase, i + 2) = Unsafe.Add(ref sBase, p2 + i);
Unsafe.Add(ref dBase, i + 3) = Unsafe.Add(ref sBase, p3 + i);
// Stage the scalar tail in a local Vector4 so p0..p3 index source
// values that were captured before any overlapping destination writes.
Vector4 v = Unsafe.As<float, Vector4>(ref Unsafe.Add(ref sBase, i));
ref float pBase = ref Unsafe.As<Vector4, float>(ref v);
Unsafe.Add(ref dBase, i + 0u) = Unsafe.Add(ref pBase, p0);
Unsafe.Add(ref dBase, i + 1u) = Unsafe.Add(ref pBase, p1);
Unsafe.Add(ref dBase, i + 2u) = Unsafe.Add(ref pBase, p2);
Unsafe.Add(ref dBase, i + 3u) = Unsafe.Add(ref pBase, p3);
}
}
[Conditional("DEBUG")]
internal static void VerifyShuffle4SpanInput<T>(ReadOnlySpan<T> source, Span<T> destination)
internal static void VerifyShuffle4SpanInput<T>(ReadOnlySpan<T> source, ReadOnlySpan<T> destination)
where T : struct
{
DebugGuard.IsTrue(
@ -173,7 +479,7 @@ internal static partial class SimdUtils
}
[Conditional("DEBUG")]
private static void VerifyShuffle3SpanInput<T>(ReadOnlySpan<T> source, Span<T> destination)
private static void VerifyShuffle3SpanInput<T>(ReadOnlySpan<T> source, ReadOnlySpan<T> destination)
where T : struct
{
DebugGuard.IsTrue(

6
src/ImageSharp/Common/Helpers/SimdUtils.cs

@ -72,7 +72,7 @@ internal static partial class SimdUtils
}
[Conditional("DEBUG")]
private static void DebugVerifySpanInput(ReadOnlySpan<byte> source, Span<float> dest, int shouldBeDivisibleBy)
private static void DebugVerifySpanInput(ReadOnlySpan<byte> source, ReadOnlySpan<float> dest, int shouldBeDivisibleBy)
{
DebugGuard.IsTrue(source.Length == dest.Length, nameof(source), "Input spans must be of same length!");
DebugGuard.IsTrue(
@ -82,7 +82,7 @@ internal static partial class SimdUtils
}
[Conditional("DEBUG")]
private static void DebugVerifySpanInput(ReadOnlySpan<float> source, Span<byte> destination, int shouldBeDivisibleBy)
private static void DebugVerifySpanInput(ReadOnlySpan<float> source, ReadOnlySpan<byte> destination, int shouldBeDivisibleBy)
{
DebugGuard.IsTrue(source.Length == destination.Length, nameof(source), "Input spans must be of same length!");
DebugGuard.IsTrue(
@ -93,9 +93,11 @@ internal static partial class SimdUtils
private struct ByteTuple4
{
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value
public byte V0;
public byte V1;
public byte V2;
public byte V3;
#pragma warning restore CS0649 // Field is never assigned to, and will always have its default value
}
}

554
src/ImageSharp/Common/Helpers/Vector128Utilities.cs

@ -74,7 +74,7 @@ internal static class Vector128_
(control >> 4) & 0x3,
(control >> 6) & 0x3);
return Vector128.Shuffle(vector, indices);
return Vector128.ShuffleNative(vector, indices);
}
/// <summary>
@ -93,7 +93,7 @@ internal static class Vector128_
(control >> 4) & 0x3,
(control >> 6) & 0x3);
return Vector128.Shuffle(vector, indices);
return Vector128.ShuffleNative(vector, indices);
}
/// <summary>
@ -124,7 +124,7 @@ internal static class Vector128_
(short)(((control >> 4) & 0x3) + 4),
(short)(((control >> 6) & 0x3) + 4));
return Vector128.Shuffle(value, indices);
return Vector128.ShuffleNative(value, indices);
}
/// <summary>
@ -155,33 +155,7 @@ internal static class Vector128_
6,
7);
return Vector128.Shuffle(value, indices);
}
/// <summary>
/// Creates a new vector by selecting values from an input vector using a set of indices.
/// </summary>
/// <param name="vector">
/// The input vector from which values are selected.</param>
/// <param name="indices">
/// The per-element indices used to select a value from <paramref name="vector" />.
/// </param>
/// <returns>
/// A new vector containing the values from <paramref name="vector" /> selected by the given <paramref name="indices" />.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> ShuffleNative(Vector128<byte> vector, Vector128<byte> indices)
{
// For x64 we use the SSSE3 shuffle intrinsic to avoid additional instructions. 3 vs 1.
if (Ssse3.IsSupported)
{
return Ssse3.Shuffle(vector, indices);
}
// For ARM and WASM, codegen will be optimal.
// We don't throw for x86/x64 so we should never use this method without
// checking for support.
return Vector128.Shuffle(vector, indices);
return Vector128.ShuffleNative(value, indices);
}
/// <summary>
@ -230,27 +204,6 @@ internal static class Vector128_
return Vector128.Shuffle(value, Vector128.Create((byte)0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15) - Vector128.Create(numBytes));
}
/// <summary>
/// Shift packed 16-bit integers in <paramref name="value"/> left by <paramref name="value"/> while
/// shifting in zeros, and store the results
/// </summary>
/// <param name="value">The vector containing packed 16-bit integers to shift.</param>
/// <param name="count">The number of bits to shift left.</param>
/// <returns>
/// A vector containing the packed 16-bit integers shifted left by <paramref name="count"/>, with zeros shifted in.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<short> ShiftLeftLogical(Vector128<short> value, [ConstantExpected] byte count)
{
// Zero lanes where count >= 16 to match SSE2
if (count >= 16)
{
return Vector128<short>.Zero;
}
return value << count;
}
/// <summary>
/// Right aligns elements of two source 128-bit values depending on bits in a mask.
/// </summary>
@ -308,60 +261,28 @@ internal static class Vector128_
}
/// <summary>
/// Rounds all values in <paramref name="vector"/> to the nearest integer
/// following <see cref="MidpointRounding.ToEven"/> semantics.
/// Converts all values in <paramref name="vector"/> to signed 32-bit integers, rounding midpoint values away from zero.
/// </summary>
/// <param name="vector">The vector</param>
/// <param name="vector">The values to convert.</param>
/// <returns>The converted integer values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<float> RoundToNearestInteger(Vector128<float> vector)
public static Vector128<int> ConvertToInt32RoundAwayFromZero(Vector128<float> vector)
{
if (Sse41.IsSupported)
if (Sse2.IsSupported)
{
return Sse41.RoundToNearestInteger(vector);
// The x86 conversion truncates, so adding one half with each lane's sign implements round-to-nearest with midpoint values away from zero.
Vector128<float> x86Adjustment = Vector128.Create(.5F) | (vector & Vector128.Create(-0F));
return Sse2.ConvertToVector128Int32WithTruncation(vector + x86Adjustment);
}
if (AdvSimd.IsSupported)
{
return AdvSimd.RoundToNearest(vector);
}
if (PackedSimd.IsSupported)
{
return PackedSimd.RoundToNearest(vector);
return AdvSimd.ConvertToInt32RoundAwayFromZero(vector);
}
Vector128<float> sign = vector & Vector128.Create(-0F);
Vector128<float> val_2p23_f32 = sign | Vector128.Create(8388608F);
val_2p23_f32 = (vector + val_2p23_f32) - val_2p23_f32;
return val_2p23_f32 | sign;
}
/// <summary>
/// Performs a multiplication and an addition of the <see cref="Vector128{Single}"/>.
/// </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>
/// <returns>The <see cref="Vector256{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<float> MultiplyAdd(
Vector128<float> va,
Vector128<float> vm0,
Vector128<float> vm1)
{
if (Fma.IsSupported)
{
return Fma.MultiplyAdd(vm1, vm0, va);
}
if (AdvSimd.IsSupported)
{
return AdvSimd.FusedMultiplyAdd(va, vm0, vm1);
}
return va + (vm0 * vm1);
Vector128<float> fallbackAdjustment = Vector128.Create(.5F) | sign;
return Vector128.ConvertToInt32(vector + fallbackAdjustment);
}
/// <summary>
@ -389,8 +310,8 @@ internal static class Vector128_
Vector128<short> min = Vector128.Create((short)byte.MinValue);
Vector128<short> max = Vector128.Create((short)byte.MaxValue);
Vector128<ushort> lefClamped = Clamp(left, min, max).AsUInt16();
Vector128<ushort> rightClamped = Clamp(right, min, max).AsUInt16();
Vector128<ushort> lefClamped = Vector128.Clamp(left, min, max).AsUInt16();
Vector128<ushort> rightClamped = Vector128.Clamp(right, min, max).AsUInt16();
return Vector128.Narrow(lefClamped, rightClamped);
}
@ -420,85 +341,11 @@ internal static class Vector128_
Vector128<int> min = Vector128.Create((int)ushort.MinValue);
Vector128<int> max = Vector128.Create((int)ushort.MaxValue);
Vector128<uint> lefClamped = Clamp(left, min, max).AsUInt32();
Vector128<uint> rightClamped = Clamp(right, min, max).AsUInt32();
return Vector128.Narrow(lefClamped, rightClamped);
}
/// <summary>
/// Packs signed 32-bit integers to signed 16-bit integers and saturates.
/// </summary>
/// <param name="left">The left hand source vector.</param>
/// <param name="right">The right hand source vector.</param>
/// <returns>The <see cref="Vector128{Int16}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<short> PackSignedSaturate(Vector128<int> left, Vector128<int> right)
{
if (Sse2.IsSupported)
{
return Sse2.PackSignedSaturate(left, right);
}
if (AdvSimd.IsSupported)
{
return AdvSimd.ExtractNarrowingSaturateUpper(AdvSimd.ExtractNarrowingSaturateLower(left), right);
}
if (PackedSimd.IsSupported)
{
return PackedSimd.ConvertNarrowingSaturateSigned(left, right);
}
Vector128<int> min = Vector128.Create((int)short.MinValue);
Vector128<int> max = Vector128.Create((int)short.MaxValue);
Vector128<int> lefClamped = Clamp(left, min, max);
Vector128<int> rightClamped = Clamp(right, min, max);
Vector128<uint> lefClamped = Vector128.Clamp(left, min, max).AsUInt32();
Vector128<uint> rightClamped = Vector128.Clamp(right, min, max).AsUInt32();
return Vector128.Narrow(lefClamped, rightClamped);
}
/// <summary>
/// Packs signed 16-bit integers to signed 8-bit integers and saturates.
/// </summary>
/// <param name="left">The left hand source vector.</param>
/// <param name="right">The right hand source vector.</param>
/// <returns>The <see cref="Vector128{Int16}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<sbyte> PackSignedSaturate(Vector128<short> left, Vector128<short> right)
{
if (Sse2.IsSupported)
{
return Sse2.PackSignedSaturate(left, right);
}
if (AdvSimd.IsSupported)
{
return AdvSimd.ExtractNarrowingSaturateUpper(AdvSimd.ExtractNarrowingSaturateLower(left), right);
}
if (PackedSimd.IsSupported)
{
return PackedSimd.ConvertNarrowingSaturateSigned(left, right);
}
Vector128<short> min = Vector128.Create((short)sbyte.MinValue);
Vector128<short> max = Vector128.Create((short)sbyte.MaxValue);
Vector128<short> lefClamped = Clamp(left, min, max);
Vector128<short> rightClamped = Clamp(right, min, max);
return Vector128.Narrow(lefClamped, rightClamped);
}
/// <summary>
/// Restricts a vector between a minimum and a maximum value.
/// </summary>
/// <typeparam name="T">The type of the elements in the vector.</typeparam>
/// <param name="value">The vector to restrict.</param>
/// <param name="min">The minimum value.</param>
/// <param name="max">The maximum value.</param>
/// <returns>The restricted <see cref="Vector128{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<T> Clamp<T>(Vector128<T> value, Vector128<T> min, Vector128<T> max)
=> Vector128.Min(Vector128.Max(value, min), max);
/// <summary>
/// Multiply packed signed 16-bit integers in <paramref name="left"/> and <paramref name="right"/>, producing
/// intermediate signed 32-bit integers. Horizontally add adjacent pairs of intermediate 32-bit integers, and
@ -793,7 +640,7 @@ internal static class Vector128_
}
Vector128<int> unpacked = Vector128.Create(left.GetUpper(), right.GetUpper());
return Vector128.Shuffle(unpacked, Vector128.Create(0, 2, 1, 3));
return Vector128.ShuffleNative(unpacked, Vector128.Create(0, 2, 1, 3));
}
/// <summary>
@ -824,7 +671,7 @@ internal static class Vector128_
}
Vector128<int> unpacked = Vector128.Create(left.GetLower(), right.GetLower());
return Vector128.Shuffle(unpacked, Vector128.Create(0, 2, 1, 3));
return Vector128.ShuffleNative(unpacked, Vector128.Create(0, 2, 1, 3));
}
/// <summary>
@ -855,7 +702,7 @@ internal static class Vector128_
}
Vector128<short> unpacked = Vector128.Create(left.GetUpper(), right.GetUpper());
return Vector128.Shuffle(unpacked, Vector128.Create(0, 4, 1, 5, 2, 6, 3, 7));
return Vector128.ShuffleNative(unpacked, Vector128.Create(0, 4, 1, 5, 2, 6, 3, 7));
}
/// <summary>
@ -886,7 +733,7 @@ internal static class Vector128_
}
Vector128<short> unpacked = Vector128.Create(left.GetLower(), right.GetLower());
return Vector128.Shuffle(unpacked, Vector128.Create(0, 4, 1, 5, 2, 6, 3, 7));
return Vector128.ShuffleNative(unpacked, Vector128.Create(0, 4, 1, 5, 2, 6, 3, 7));
}
/// <summary>
@ -917,7 +764,7 @@ internal static class Vector128_
}
Vector128<byte> unpacked = Vector128.Create(left.GetUpper(), right.GetUpper());
return Vector128.Shuffle(unpacked, Vector128.Create((byte)0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15));
return Vector128.ShuffleNative(unpacked, Vector128.Create((byte)0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15));
}
/// <summary>
@ -948,7 +795,7 @@ internal static class Vector128_
}
Vector128<byte> unpacked = Vector128.Create(left.GetLower(), right.GetLower());
return Vector128.Shuffle(unpacked, Vector128.Create((byte)0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15));
return Vector128.ShuffleNative(unpacked, Vector128.Create((byte)0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15));
}
/// <summary>
@ -979,7 +826,7 @@ internal static class Vector128_
}
Vector128<sbyte> unpacked = Vector128.Create(left.GetUpper(), right.GetUpper());
return Vector128.Shuffle(unpacked, Vector128.Create(0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15));
return Vector128.ShuffleNative(unpacked, Vector128.Create(0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15));
}
/// <summary>
@ -1010,353 +857,6 @@ internal static class Vector128_
}
Vector128<sbyte> unpacked = Vector128.Create(left.GetLower(), right.GetLower());
return Vector128.Shuffle(unpacked, Vector128.Create(0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15));
}
/// <summary>
/// Subtract packed signed 16-bit integers in <paramref name="right"/> from packed signed 16-bit integers
/// in <paramref name="left"/> using saturation, and store the results.
/// </summary>
/// <param name="left">
/// The first vector containing packed signed 16-bit integers to subtract from.
/// </param>
/// <param name="right">
/// The second vector containing packed signed 16-bit integers to subtract.
/// </param>
/// <returns>
/// A vector containing the results of subtracting packed signed 16-bit integers
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<short> SubtractSaturate(Vector128<short> left, Vector128<short> right)
{
if (Sse2.IsSupported)
{
return Sse2.SubtractSaturate(left, right);
}
if (AdvSimd.IsSupported)
{
return AdvSimd.SubtractSaturate(left, right);
}
if (PackedSimd.IsSupported)
{
return PackedSimd.SubtractSaturate(left, right);
}
// Widen inputs to 32-bit signed
(Vector128<int> leftLo, Vector128<int> leftHi) = Vector128.Widen(left);
(Vector128<int> rightLo, Vector128<int> rightHi) = Vector128.Widen(right);
// Subtract
Vector128<int> diffLo = leftLo - rightLo;
Vector128<int> diffHi = leftHi - rightHi;
// Clamp to signed 16-bit range
Vector128<int> min = Vector128.Create((int)short.MinValue);
Vector128<int> max = Vector128.Create((int)short.MaxValue);
diffLo = Clamp(diffLo, min, max);
diffHi = Clamp(diffHi, min, max);
// Narrow back to 16 bit signed.
return Vector128.Narrow(diffLo, diffHi);
}
/// <summary>
/// Subtract packed unsigned 16-bit integers in <paramref name="right"/> from packed unsigned 16-bit integers
/// in <paramref name="left"/> using saturation, and store the results.
/// </summary>
/// <param name="left">
/// The first vector containing packed unsigned 16-bit integers to subtract from.
/// </param>
/// <param name="right">
/// The second vector containing packed unsigned 16-bit integers to subtract.
/// </param>
/// <returns>
/// A vector containing the results of subtracting packed unsigned 16-bit integers
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<ushort> SubtractSaturate(Vector128<ushort> left, Vector128<ushort> right)
{
if (Sse2.IsSupported)
{
return Sse2.SubtractSaturate(left, right);
}
if (AdvSimd.IsSupported)
{
return AdvSimd.SubtractSaturate(left, right);
}
if (PackedSimd.IsSupported)
{
return PackedSimd.SubtractSaturate(left, right);
}
// Widen inputs to 32-bit signed
(Vector128<uint> leftLo, Vector128<uint> leftHi) = Vector128.Widen(left);
(Vector128<uint> rightLo, Vector128<uint> rightHi) = Vector128.Widen(right);
// Subtract
Vector128<uint> diffLo = leftLo - rightLo;
Vector128<uint> diffHi = leftHi - rightHi;
// Clamp to signed 16-bit range
Vector128<uint> min = Vector128.Create((uint)ushort.MinValue);
Vector128<uint> max = Vector128.Create((uint)ushort.MaxValue);
diffLo = Clamp(diffLo, min, max);
diffHi = Clamp(diffHi, min, max);
// Narrow back to 16 bit signed.
return Vector128.Narrow(diffLo, diffHi);
}
/// <summary>
/// Add packed unsigned 8-bit integers in <paramref name="right"/> to packed unsigned 8-bit integers
/// in <paramref name="left"/> using saturation, and store the results.
/// </summary>
/// <param name="left">
/// The first vector containing packed unsigned 8-bit integers to add to.
/// </param>
/// <param name="right">
/// The second vector containing packed unsigned 8-bit integers to add.
/// </param>
/// <returns>
/// A vector containing the results of adding packed unsigned 8-bit integers
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> AddSaturate(Vector128<byte> left, Vector128<byte> right)
{
if (Sse2.IsSupported)
{
return Sse2.AddSaturate(left, right);
}
if (AdvSimd.IsSupported)
{
return AdvSimd.AddSaturate(left, right);
}
if (PackedSimd.IsSupported)
{
return PackedSimd.AddSaturate(left, right);
}
// Widen inputs to 16-bit
(Vector128<ushort> leftLo, Vector128<ushort> leftHi) = Vector128.Widen(left);
(Vector128<ushort> rightLo, Vector128<ushort> rightHi) = Vector128.Widen(right);
// Add
Vector128<ushort> sumLo = leftLo + rightLo;
Vector128<ushort> sumHi = leftHi + rightHi;
// Clamp to signed 8-bit range
Vector128<ushort> max = Vector128.Create((ushort)byte.MaxValue);
sumLo = Clamp(sumLo, Vector128<ushort>.Zero, max);
sumHi = Clamp(sumHi, Vector128<ushort>.Zero, max);
// Narrow back to bytes
return Vector128.Narrow(sumLo, sumHi);
}
/// <summary>
/// Add packed unsigned 16-bit integers in <paramref name="right"/> to packed unsigned 16-bit integers
/// in <paramref name="left"/> using saturation, and store the results.
/// </summary>
/// <param name="left">
/// The first vector containing packed unsigned 16-bit integers to add to.
/// </param>
/// <param name="right">
/// The second vector containing packed unsigned 16-bit integers to add.
/// </param>
/// <returns>
/// A vector containing the results of adding packed unsigned 16-bit integers
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<ushort> AddSaturate(Vector128<ushort> left, Vector128<ushort> right)
{
if (Sse2.IsSupported)
{
return Sse2.AddSaturate(left, right);
}
if (AdvSimd.IsSupported)
{
return AdvSimd.AddSaturate(left, right);
}
if (PackedSimd.IsSupported)
{
return PackedSimd.AddSaturate(left, right);
}
// Widen inputs to 32-bit
(Vector128<uint> leftLo, Vector128<uint> leftHi) = Vector128.Widen(left);
(Vector128<uint> rightLo, Vector128<uint> rightHi) = Vector128.Widen(right);
// Add
Vector128<uint> sumLo = leftLo + rightLo;
Vector128<uint> sumHi = leftHi + rightHi;
// Clamp to signed 16-bit range
Vector128<uint> max = Vector128.Create((uint)ushort.MaxValue);
sumLo = Clamp(sumLo, Vector128<uint>.Zero, max);
sumHi = Clamp(sumHi, Vector128<uint>.Zero, max);
// Narrow back to 16 bit unsigned.
return Vector128.Narrow(sumLo, sumHi);
}
/// <summary>
/// Subtract packed unsigned 8-bit integers in <paramref name="right"/> from packed unsigned 8-bit integers
/// in <paramref name="left"/> using saturation, and store the results.
/// </summary>
/// <param name="left">
/// The first vector containing packed unsigned 8-bit integers to subtract from.
/// </param>
/// <param name="right">
/// The second vector containing packed unsigned 8-bit integers to subtract.
/// </param>
/// <returns>
/// A vector containing the results of subtracting packed unsigned 8-bit integers
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<byte> SubtractSaturate(Vector128<byte> left, Vector128<byte> right)
{
if (Sse2.IsSupported)
{
return Sse2.SubtractSaturate(left, right);
}
if (AdvSimd.IsSupported)
{
return AdvSimd.SubtractSaturate(left, right);
}
if (PackedSimd.IsSupported)
{
return PackedSimd.SubtractSaturate(left, right);
}
// Widen inputs to 16-bit
(Vector128<ushort> leftLo, Vector128<ushort> leftHi) = Vector128.Widen(left);
(Vector128<ushort> rightLo, Vector128<ushort> rightHi) = Vector128.Widen(right);
// Subtract
Vector128<ushort> diffLo = leftLo - rightLo;
Vector128<ushort> diffHi = leftHi - rightHi;
// Clamp to signed 8-bit range
Vector128<ushort> max = Vector128.Create((ushort)byte.MaxValue);
diffLo = Clamp(diffLo, Vector128<ushort>.Zero, max);
diffHi = Clamp(diffHi, Vector128<ushort>.Zero, max);
// Narrow back to bytes
return Vector128.Narrow(diffLo, diffHi);
}
/// <summary>
/// Add packed unsigned 8-bit integers in <paramref name="right"/> from packed unsigned 8-bit integers
/// in <paramref name="left"/> using saturation, and store the results.
/// </summary>
/// <param name="left">
/// The first vector containing packed unsigned 8-bit integers to add to.
/// </param>
/// <param name="right">
/// The second vector containing packed unsigned 8-bit integers to add.
/// </param>
/// <returns>
/// A vector containing the results of adding packed unsigned 8-bit integers
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<sbyte> AddSaturate(Vector128<sbyte> left, Vector128<sbyte> right)
{
if (Sse2.IsSupported)
{
return Sse2.AddSaturate(left, right);
}
if (AdvSimd.IsSupported)
{
return AdvSimd.AddSaturate(left, right);
}
if (PackedSimd.IsSupported)
{
return PackedSimd.AddSaturate(left, right);
}
// Widen inputs to 16-bit
(Vector128<short> leftLo, Vector128<short> leftHi) = Vector128.Widen(left);
(Vector128<short> rightLo, Vector128<short> rightHi) = Vector128.Widen(right);
// Add
Vector128<short> sumLo = leftLo + rightLo;
Vector128<short> sumHi = leftHi + rightHi;
// Clamp to signed 8-bit range
Vector128<short> min = Vector128.Create((short)sbyte.MinValue);
Vector128<short> max = Vector128.Create((short)sbyte.MaxValue);
sumLo = Clamp(sumLo, min, max);
sumHi = Clamp(sumHi, min, max);
// Narrow back to signed bytes
return Vector128.Narrow(sumLo, sumHi);
}
/// <summary>
/// Subtract packed signed 8-bit integers in <paramref name="right"/> from packed signed 8-bit integers
/// in <paramref name="left"/> using saturation, and store the results.
/// </summary>
/// <param name="left">
/// The first vector containing packed signed 8-bit integers to subtract from.
/// </param>
/// <param name="right">
/// The second vector containing packed signed 8-bit integers to subtract.
/// </param>
/// <returns>
/// A vector containing the results of subtracting packed signed 8-bit integers
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<sbyte> SubtractSaturate(Vector128<sbyte> left, Vector128<sbyte> right)
{
if (Sse2.IsSupported)
{
return Sse2.SubtractSaturate(left, right);
}
if (AdvSimd.IsSupported)
{
return AdvSimd.SubtractSaturate(left, right);
}
if (PackedSimd.IsSupported)
{
return PackedSimd.SubtractSaturate(left, right);
}
// Widen inputs to 16-bit
(Vector128<short> leftLo, Vector128<short> leftHi) = Vector128.Widen(left);
(Vector128<short> rightLo, Vector128<short> rightHi) = Vector128.Widen(right);
// Subtract
Vector128<short> diffLo = leftLo - rightLo;
Vector128<short> diffHi = leftHi - rightHi;
// Clamp to signed 8-bit range
Vector128<short> min = Vector128.Create((short)sbyte.MinValue);
Vector128<short> max = Vector128.Create((short)sbyte.MaxValue);
diffLo = Clamp(diffLo, min, max);
diffHi = Clamp(diffHi, min, max);
// Narrow back to signed bytes
return Vector128.Narrow(diffLo, diffHi);
return Vector128.ShuffleNative(unpacked, Vector128.Create(0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15));
}
}

126
src/ImageSharp/Common/Helpers/Vector256Utilities.cs

@ -31,13 +31,11 @@ internal static class Vector256_
=> Avx.Shuffle(vector, vector, control);
/// <summary>
/// Creates a new vector by selecting values from an input vector using a set of indices.</summary>
/// <param name="vector">
/// The input vector from which values are selected.</param>
/// <param name="indices">
/// The per-element indices used to select a value from <paramref name="vector" />.
/// </param>
/// <returns>The <see cref="Vector256{Single}"/>.</returns>
/// Creates a new vector by selecting values from each 128-bit input lane using the corresponding indices.
/// </summary>
/// <param name="vector">The input vector from which values are selected.</param>
/// <param name="indices">The per-element indices used to select values within each 128-bit lane.</param>
/// <returns>The shuffled <see cref="Vector256{Byte}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<byte> ShufflePerLane(Vector256<byte> vector, Vector256<byte> indices)
{
@ -46,9 +44,11 @@ internal static class Vector256_
return Avx2.Shuffle(vector, indices);
}
// The .NET 10 fallback treats indices as full-width when AVX2 is unavailable. Reusing
// the low mask for each half preserves the lane-local vpshufb contract on AVX-only CPUs.
Vector128<byte> indicesLo = indices.GetLower();
Vector128<byte> lower = Vector128_.ShuffleNative(vector.GetLower(), indicesLo);
Vector128<byte> upper = Vector128_.ShuffleNative(vector.GetUpper(), indicesLo);
Vector128<byte> lower = Vector128.ShuffleNative(vector.GetLower(), indicesLo);
Vector128<byte> upper = Vector128.ShuffleNative(vector.GetUpper(), indicesLo);
return Vector256.Create(lower, upper);
}
@ -74,45 +74,45 @@ internal static class Vector256_
}
/// <summary>
/// Rounds all values in <paramref name="vector"/> to the nearest integer
/// following <see cref="MidpointRounding.ToEven"/> semantics.
/// Converts all values in <paramref name="vector"/> to signed 32-bit integers, rounding midpoint values away from zero.
/// </summary>
/// <param name="vector">The vector</param>
/// <param name="vector">The values to convert.</param>
/// <returns>The converted integer values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<float> RoundToNearestInteger(Vector256<float> vector)
public static Vector256<int> ConvertToInt32RoundAwayFromZero(Vector256<float> vector)
{
if (Avx.IsSupported)
{
return Avx.RoundToNearestInteger(vector);
// The x86 conversion truncates, so adding one half with each lane's sign implements round-to-nearest with midpoint values away from zero.
Vector256<float> x86Adjustment = Vector256.Create(.5F) | (vector & Vector256.Create(-0F));
return Avx.ConvertToVector256Int32WithTruncation(vector + x86Adjustment);
}
Vector256<float> sign = vector & Vector256.Create(-0F);
Vector256<float> val_2p23_f32 = sign | Vector256.Create(8388608F);
val_2p23_f32 = (vector + val_2p23_f32) - val_2p23_f32;
return val_2p23_f32 | sign;
Vector256<float> fallbackAdjustment = Vector256.Create(.5F) | sign;
return Vector256.ConvertToInt32(vector + fallbackAdjustment);
}
/// <summary>
/// Performs a multiplication and an addition of the <see cref="Vector256{Single}"/>.
/// Performs a multiplication and a negated addition of the <see cref="Vector256{Single}"/>.
/// </summary>
/// <remarks>ret = (vm0 * vm1) + va</remarks>
/// <param name="va">The vector to add to the intermediate result.</param>
/// <remarks>ret = va - (vm0 * vm1)</remarks>
/// <param name="va">The vector to add to the negated 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(MethodImplOptions.AggressiveInlining)]
public static Vector256<float> MultiplyAdd(
[MethodImpl(InliningOptions.ShortMethod)]
public static Vector256<float> MultiplyAddNegated(
Vector256<float> va,
Vector256<float> vm0,
Vector256<float> vm1)
{
if (Fma.IsSupported)
{
return Fma.MultiplyAdd(vm0, vm1, va);
return Fma.MultiplyAddNegated(vm0, vm1, va);
}
return va + (vm0 * vm1);
return va - (vm0 * vm1);
}
/// <summary>
@ -180,8 +180,8 @@ internal static class Vector256_
Vector256<int> min = Vector256.Create((int)ushort.MinValue);
Vector256<int> max = Vector256.Create((int)ushort.MaxValue);
Vector256<uint> lefClamped = Clamp(left, min, max).AsUInt32();
Vector256<uint> rightClamped = Clamp(right, min, max).AsUInt32();
Vector256<uint> lefClamped = Vector256.Clamp(left, min, max).AsUInt32();
Vector256<uint> rightClamped = Vector256.Clamp(right, min, max).AsUInt32();
return Vector256.Narrow(lefClamped, rightClamped);
}
@ -201,8 +201,8 @@ internal static class Vector256_
Vector256<int> min = Vector256.Create((int)short.MinValue);
Vector256<int> max = Vector256.Create((int)short.MaxValue);
Vector256<int> lefClamped = Clamp(left, min, max);
Vector256<int> rightClamped = Clamp(right, min, max);
Vector256<int> lefClamped = Vector256.Clamp(left, min, max);
Vector256<int> rightClamped = Vector256.Clamp(right, min, max);
return Vector256.Narrow(lefClamped, rightClamped);
}
@ -222,23 +222,11 @@ internal static class Vector256_
Vector256<short> min = Vector256.Create((short)sbyte.MinValue);
Vector256<short> max = Vector256.Create((short)sbyte.MaxValue);
Vector256<short> lefClamped = Clamp(left, min, max);
Vector256<short> rightClamped = Clamp(right, min, max);
Vector256<short> lefClamped = Vector256.Clamp(left, min, max);
Vector256<short> rightClamped = Vector256.Clamp(right, min, max);
return Vector256.Narrow(lefClamped, rightClamped);
}
/// <summary>
/// Restricts a vector between a minimum and a maximum value.
/// </summary>
/// <typeparam name="T">The type of the elements in the vector.</typeparam>
/// <param name="value">The vector to restrict.</param>
/// <param name="min">The minimum value.</param>
/// <param name="max">The maximum value.</param>
/// <returns>The restricted <see cref="Vector256{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<T> Clamp<T>(Vector256<T> value, Vector256<T> min, Vector256<T> max)
=> Vector256.Min(Vector256.Max(value, min), max);
/// <summary>
/// Widens a <see cref="Vector128{Int16}"/> to a <see cref="Vector256{Int32}"/>.
/// </summary>
@ -409,56 +397,4 @@ internal static class Vector256_
return Vector256.Create(lo, hi);
}
/// <summary>
/// Subtract packed signed 16-bit integers in <paramref name="right"/> from packed signed 16-bit integers
/// in <paramref name="left"/> using saturation, and store the results.
/// </summary>
/// <param name="left">
/// The first vector containing packed signed 16-bit integers to subtract from.
/// </param>
/// <param name="right">
/// The second vector containing packed signed 16-bit integers to subtract.
/// </param>
/// <returns>
/// A vector containing the results of subtracting packed unsigned 16-bit integers
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<short> SubtractSaturate(Vector256<short> left, Vector256<short> right)
{
if (Avx2.IsSupported)
{
return Avx2.SubtractSaturate(left, right);
}
return Vector256.Create(
Vector128_.SubtractSaturate(left.GetLower(), right.GetLower()),
Vector128_.SubtractSaturate(left.GetUpper(), right.GetUpper()));
}
/// <summary>
/// Subtract packed unsigned 8-bit integers in <paramref name="right"/> from packed unsigned 8-bit integers
/// in <paramref name="left"/> using saturation, and store the results.
/// </summary>
/// <param name="left">
/// The first vector containing packed unsigned 8-bit integers to subtract from.
/// </param>
/// <param name="right">
/// The second vector containing packed unsigned 8-bit integers to subtract.
/// </param>
/// <returns>
/// A vector containing the results of subtracting packed unsigned 8-bit integers
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<byte> SubtractSaturate(Vector256<byte> left, Vector256<byte> right)
{
if (Avx2.IsSupported)
{
return Avx2.SubtractSaturate(left, right);
}
return Vector256.Create(
Vector128_.SubtractSaturate(left.GetLower(), right.GetLower()),
Vector128_.SubtractSaturate(left.GetUpper(), right.GetUpper()));
}
}

61
src/ImageSharp/Common/Helpers/Vector512Utilities.cs

@ -30,25 +30,6 @@ internal static class Vector512_
public static Vector512<float> ShuffleNative(Vector512<float> vector, [ConstantExpected] byte control)
=> Avx512F.Shuffle(vector, vector, control);
/// <summary>
/// Creates a new vector by selecting values from an input vector using a set of indices.
/// </summary>
/// <param name="vector">The input vector from which values are selected.</param>
/// <param name="indices">
/// The per-element indices used to select a value from <paramref name="vector" />.
/// </param>
/// <returns>The <see cref="Vector512{Byte}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<byte> ShuffleNative(Vector512<byte> vector, Vector512<byte> indices)
{
if (Avx512BW.IsSupported)
{
return Avx512BW.Shuffle(vector, indices);
}
return Vector512.Shuffle(vector, indices);
}
/// <summary>
/// Performs a conversion from a 512-bit vector of 16 single-precision floating-point values to a 512-bit vector of 16 signed 32-bit integer values.
/// Rounding is equivalent to <see cref="MidpointRounding.ToEven"/>.
@ -60,42 +41,30 @@ internal static class Vector512_
=> Avx512F.ConvertToVector512Int32(vector);
/// <summary>
/// Rounds all values in <paramref name="vector"/> to the nearest integer
/// following <see cref="MidpointRounding.ToEven"/> semantics.
/// Converts all values in <paramref name="vector"/> to signed 32-bit integers, rounding midpoint values away from zero.
/// </summary>
/// <param name="vector">The vector</param>
/// <param name="vector">The values to convert.</param>
/// <returns>The converted integer values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<float> RoundToNearestInteger(Vector512<float> vector)
// imm8 = 0b1000:
// imm8[7:4] = 0b0000 -> preserve 0 fractional bits (round to whole numbers)
// imm8[3:0] = 0b1000 -> _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC (round to nearest even, suppress exceptions)
=> Avx512F.RoundScale(vector, 0b0000_1000);
public static Vector512<int> ConvertToInt32RoundAwayFromZero(Vector512<float> vector)
{
// The x86 conversion truncates, so adding one half with each lane's sign implements round-to-nearest with midpoint values away from zero.
Vector512<float> half = Vector512.Create(.5F) | (vector & Vector512.Create(-0F));
return Avx512F.ConvertToVector512Int32WithTruncation(vector + half);
}
/// <summary>
/// Performs a multiplication and an addition of the <see cref="Vector512{Single}"/>.
/// Performs a multiplication and a negated addition of the <see cref="Vector512{Single}"/>.
/// </summary>
/// <remarks>ret = (vm0 * vm1) + va</remarks>
/// <param name="va">The vector to add to the intermediate result.</param>
/// <remarks>ret = va - (vm0 * vm1)</remarks>
/// <param name="va">The vector to add to the negated 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>
/// <returns>The <see cref="Vector512{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<float> MultiplyAdd(
public static Vector512<float> MultiplyAddNegated(
Vector512<float> va,
Vector512<float> vm0,
Vector512<float> vm1)
=> Avx512F.FusedMultiplyAdd(vm0, vm1, va);
/// <summary>
/// Restricts a vector between a minimum and a maximum value.
/// </summary>
/// <typeparam name="T">The type of the elements in the vector.</typeparam>
/// <param name="value">The vector to restrict.</param>
/// <param name="min">The minimum value.</param>
/// <param name="max">The maximum value.</param>
/// <returns>The restricted <see cref="Vector512{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<T> Clamp<T>(Vector512<T> value, Vector512<T> min, Vector512<T> max)
=> Vector512.Min(Vector512.Max(value, min), max);
=> Avx512F.FusedMultiplyAddNegated(vm0, vm1, va);
}

52
src/ImageSharp/Common/InlineArray.cs

@ -26,6 +26,15 @@ internal struct InlineArray8<T>
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 14 elements.
/// </summary>
[InlineArray(14)]
internal struct InlineArray14<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 16 elements.
/// </summary>
@ -35,4 +44,47 @@ internal struct InlineArray16<T>
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 18 elements.
/// </summary>
[InlineArray(18)]
internal struct InlineArray18<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 19 elements.
/// </summary>
[InlineArray(19)]
internal struct InlineArray19<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 26 elements.
/// </summary>
[InlineArray(26)]
internal struct InlineArray26<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 36 elements.
/// </summary>
[InlineArray(36)]
internal struct InlineArray36<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 256 elements.
/// </summary>
[InlineArray(256)]
internal struct InlineArray256<T>
{
private T t;
}

2
src/ImageSharp/Common/InlineArray.tt

@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp;
<#GenerateInlineArrays();#>
<#+
private static int[] Lengths = [4, 8, 16 ];
private static int[] Lengths = [4, 8, 14, 16, 18, 19, 26, 36, 256];
void GenerateInlineArrays()
{

72
src/ImageSharp/Common/Tuples/Octet{T}.cs

@ -1,72 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Tuples;
/// <summary>
/// Contains 8 element value tuples of various types.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct Octet<T>
where T : unmanaged
{
public T V0;
public T V1;
public T V2;
public T V3;
public T V4;
public T V5;
public T V6;
public T V7;
/// <inheritdoc/>
public override readonly string ToString()
{
return $"Octet<{typeof(T)}>({this.V0},{this.V1},{this.V2},{this.V3},{this.V4},{this.V5},{this.V6},{this.V7})";
}
}
/// <summary>
/// Extension methods for the <see cref="Octet{T}"/> type.
/// </summary>
internal static class OctetExtensions
{
/// <summary>
/// Loads the fields in a target <see cref="Octet{T}"/> of <see cref="uint"/> from one of <see cref="byte"/> type.
/// </summary>
/// <param name="destination">The target <see cref="Octet{T}"/> of <see cref="uint"/> instance.</param>
/// <param name="source">The source <see cref="Octet{T}"/> of <see cref="byte"/> instance.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static void LoadFrom(ref this Octet<uint> destination, ref Octet<byte> source)
{
destination.V0 = source.V0;
destination.V1 = source.V1;
destination.V2 = source.V2;
destination.V3 = source.V3;
destination.V4 = source.V4;
destination.V5 = source.V5;
destination.V6 = source.V6;
destination.V7 = source.V7;
}
/// <summary>
/// Loads the fields in a target <see cref="Octet{T}"/> of <see cref="byte"/> from one of <see cref="uint"/> type.
/// </summary>
/// <param name="destination">The target <see cref="Octet{T}"/> of <see cref="byte"/> instance.</param>
/// <param name="source">The source <see cref="Octet{T}"/> of <see cref="uint"/> instance.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static void LoadFrom(ref this Octet<byte> destination, ref Octet<uint> source)
{
destination.V0 = (byte)source.V0;
destination.V1 = (byte)source.V1;
destination.V2 = (byte)source.V2;
destination.V3 = (byte)source.V3;
destination.V4 = (byte)source.V4;
destination.V5 = (byte)source.V5;
destination.V6 = (byte)source.V6;
destination.V7 = (byte)source.V7;
}
}

435
src/ImageSharp/Compression/Zlib/Adler32.cs

@ -1,435 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86;
#pragma warning disable IDE0007 // Use implicit type
namespace SixLabors.ImageSharp.Compression.Zlib;
/// <summary>
/// Calculates the 32 bit Adler checksum of a given buffer according to
/// RFC 1950. ZLIB Compressed Data Format Specification version 3.3)
/// </summary>
internal static class Adler32
{
/// <summary>
/// The default initial seed value of a Adler32 checksum calculation.
/// </summary>
public const uint SeedValue = 1U;
// Largest prime smaller than 65536
private const uint BASE = 65521;
// NMAX is the largest n such that 255n(n+1)/2 + (n+1)(BASE-1) <= 2^32-1
private const uint NMAX = 5552;
private const int MinBufferSize = 64;
private const int BlockSize = 1 << 5;
// The C# compiler emits this as a compile-time constant embedded in the PE file.
private static ReadOnlySpan<byte> Tap1Tap2 =>
[
32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, // tap1
16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 // tap2
];
/// <summary>
/// Calculates the Adler32 checksum with the bytes taken from the span.
/// </summary>
/// <param name="buffer">The readonly span of bytes.</param>
/// <returns>The <see cref="uint"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static uint Calculate(ReadOnlySpan<byte> buffer)
=> Calculate(SeedValue, buffer);
/// <summary>
/// Calculates the Adler32 checksum with the bytes taken from the span and seed.
/// </summary>
/// <param name="adler">The input Adler32 value.</param>
/// <param name="buffer">The readonly span of bytes.</param>
/// <returns>The <see cref="uint"/>.</returns>
[MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)]
public static uint Calculate(uint adler, ReadOnlySpan<byte> buffer)
{
if (buffer.IsEmpty)
{
return adler;
}
if (Avx2.IsSupported && buffer.Length >= MinBufferSize)
{
return CalculateAvx2(adler, buffer);
}
if (Ssse3.IsSupported && buffer.Length >= MinBufferSize)
{
return CalculateSse(adler, buffer);
}
if (AdvSimd.IsSupported)
{
return CalculateArm(adler, buffer);
}
return CalculateScalar(adler, buffer);
}
// Based on https://github.com/chromium/chromium/blob/master/third_party/zlib/adler32_simd.c
[MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)]
private static unsafe uint CalculateSse(uint adler, ReadOnlySpan<byte> buffer)
{
uint s1 = adler & 0xFFFF;
uint s2 = (adler >> 16) & 0xFFFF;
// Process the data in blocks.
uint length = (uint)buffer.Length;
uint blocks = length / BlockSize;
length -= blocks * BlockSize;
fixed (byte* bufferPtr = &MemoryMarshal.GetReference(buffer))
{
fixed (byte* tapPtr = &MemoryMarshal.GetReference(Tap1Tap2))
{
byte* localBufferPtr = bufferPtr;
// _mm_setr_epi8 on x86
Vector128<sbyte> tap1 = Sse2.LoadVector128((sbyte*)tapPtr);
Vector128<sbyte> tap2 = Sse2.LoadVector128((sbyte*)(tapPtr + 0x10));
Vector128<byte> zero = Vector128<byte>.Zero;
Vector128<short> ones = Vector128.Create((short)1);
while (blocks > 0)
{
uint n = NMAX / BlockSize; /* The NMAX constraint. */
if (n > blocks)
{
n = blocks;
}
blocks -= n;
// Process n blocks of data. At most NMAX data bytes can be
// processed before s2 must be reduced modulo BASE.
Vector128<uint> v_ps = Vector128.CreateScalar(s1 * n);
Vector128<uint> v_s2 = Vector128.CreateScalar(s2);
Vector128<uint> v_s1 = Vector128<uint>.Zero;
do
{
// Load 32 input bytes.
Vector128<byte> bytes1 = Sse3.LoadDquVector128(localBufferPtr);
Vector128<byte> bytes2 = Sse3.LoadDquVector128(localBufferPtr + 0x10);
// Add previous block byte sum to v_ps.
v_ps = Sse2.Add(v_ps, v_s1);
// Horizontally add the bytes for s1, multiply-adds the
// bytes by [ 32, 31, 30, ... ] for s2.
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes1, zero).AsUInt32());
Vector128<short> mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1);
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad1, ones).AsUInt32());
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes2, zero).AsUInt32());
Vector128<short> mad2 = Ssse3.MultiplyAddAdjacent(bytes2, tap2);
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad2, ones).AsUInt32());
localBufferPtr += BlockSize;
}
while (--n > 0);
v_s2 = Sse2.Add(v_s2, Sse2.ShiftLeftLogical(v_ps, 5));
// Sum epi32 ints v_s1(s2) and accumulate in s1(s2).
const byte s2301 = 0b1011_0001; // A B C D -> B A D C
const byte s1032 = 0b0100_1110; // A B C D -> C D A B
v_s1 = Sse2.Add(v_s1, Sse2.Shuffle(v_s1, s1032));
s1 += v_s1.ToScalar();
v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, s2301));
v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, s1032));
s2 = v_s2.ToScalar();
// Reduce.
s1 %= BASE;
s2 %= BASE;
}
if (length > 0)
{
HandleLeftOver(localBufferPtr, length, ref s1, ref s2);
}
return s1 | (s2 << 16);
}
}
}
// Based on: https://github.com/zlib-ng/zlib-ng/blob/develop/arch/x86/adler32_avx2.c
[MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)]
public static unsafe uint CalculateAvx2(uint adler, ReadOnlySpan<byte> buffer)
{
uint s1 = adler & 0xFFFF;
uint s2 = (adler >> 16) & 0xFFFF;
uint length = (uint)buffer.Length;
fixed (byte* bufferPtr = &MemoryMarshal.GetReference(buffer))
{
byte* localBufferPtr = bufferPtr;
Vector256<byte> zero = Vector256<byte>.Zero;
Vector256<short> dot3v = Vector256.Create((short)1);
Vector256<sbyte> dot2v = Vector256.Create(32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1);
// Process n blocks of data. At most NMAX data bytes can be
// processed before s2 must be reduced modulo BASE.
Vector256<uint> vs1 = Vector256.CreateScalar(s1);
Vector256<uint> vs2 = Vector256.CreateScalar(s2);
while (length >= 32)
{
int k = length < NMAX ? (int)length : (int)NMAX;
k -= k % 32;
length -= (uint)k;
Vector256<uint> vs10 = vs1;
Vector256<uint> vs3 = Vector256<uint>.Zero;
while (k >= 32)
{
// Load 32 input bytes.
Vector256<byte> block = Avx.LoadVector256(localBufferPtr);
// Sum of abs diff, resulting in 2 x int32's
Vector256<ushort> vs1sad = Avx2.SumAbsoluteDifferences(block, zero);
vs1 = Avx2.Add(vs1, vs1sad.AsUInt32());
vs3 = Avx2.Add(vs3, vs10);
// sum 32 uint8s to 16 shorts.
Vector256<short> vshortsum2 = Avx2.MultiplyAddAdjacent(block, dot2v);
// sum 16 shorts to 8 uint32s.
Vector256<int> vsum2 = Avx2.MultiplyAddAdjacent(vshortsum2, dot3v);
vs2 = Avx2.Add(vsum2.AsUInt32(), vs2);
vs10 = vs1;
localBufferPtr += BlockSize;
k -= 32;
}
// Defer the multiplication with 32 to outside of the loop.
vs3 = Avx2.ShiftLeftLogical(vs3, 5);
vs2 = Avx2.Add(vs2, vs3);
s1 = (uint)Numerics.EvenReduceSum(vs1.AsInt32());
s2 = (uint)Numerics.ReduceSum(vs2.AsInt32());
s1 %= BASE;
s2 %= BASE;
vs1 = Vector256.CreateScalar(s1);
vs2 = Vector256.CreateScalar(s2);
}
if (length > 0)
{
HandleLeftOver(localBufferPtr, length, ref s1, ref s2);
}
return s1 | (s2 << 16);
}
}
// Based on: https://github.com/chromium/chromium/blob/master/third_party/zlib/adler32_simd.c
[MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)]
private static unsafe uint CalculateArm(uint adler, ReadOnlySpan<byte> buffer)
{
// Split Adler-32 into component sums.
uint s1 = adler & 0xFFFF;
uint s2 = (adler >> 16) & 0xFFFF;
uint length = (uint)buffer.Length;
// Process the data in blocks.
long blocks = length / BlockSize;
length -= (uint)(blocks * BlockSize);
fixed (byte* bufferPtr = &MemoryMarshal.GetReference(buffer))
{
byte* localBufferPtr = bufferPtr;
while (blocks != 0)
{
uint n = NMAX / BlockSize;
if (n > blocks)
{
n = (uint)blocks;
}
blocks -= n;
// Process n blocks of data. At most nMax data bytes can be
// processed before s2 must be reduced modulo Base.
Vector128<uint> vs1 = Vector128<uint>.Zero;
Vector128<uint> vs2 = vs1.WithElement(3, s1 * n);
Vector128<ushort> vColumnSum1 = Vector128<ushort>.Zero;
Vector128<ushort> vColumnSum2 = Vector128<ushort>.Zero;
Vector128<ushort> vColumnSum3 = Vector128<ushort>.Zero;
Vector128<ushort> vColumnSum4 = Vector128<ushort>.Zero;
do
{
// Load 32 input bytes.
Vector128<ushort> bytes1 = AdvSimd.LoadVector128(localBufferPtr).AsUInt16();
Vector128<ushort> bytes2 = AdvSimd.LoadVector128(localBufferPtr + 0x10).AsUInt16();
// Add previous block byte sum to v_s2.
vs2 = AdvSimd.Add(vs2, vs1);
// Horizontally add the bytes for s1.
vs1 = AdvSimd.AddPairwiseWideningAndAdd(
vs1.AsUInt32(),
AdvSimd.AddPairwiseWideningAndAdd(AdvSimd.AddPairwiseWidening(bytes1.AsByte()).AsUInt16(), bytes2.AsByte()));
// Vertically add the bytes for s2.
vColumnSum1 = AdvSimd.AddWideningLower(vColumnSum1, bytes1.GetLower().AsByte());
vColumnSum2 = AdvSimd.AddWideningLower(vColumnSum2, bytes1.GetUpper().AsByte());
vColumnSum3 = AdvSimd.AddWideningLower(vColumnSum3, bytes2.GetLower().AsByte());
vColumnSum4 = AdvSimd.AddWideningLower(vColumnSum4, bytes2.GetUpper().AsByte());
localBufferPtr += BlockSize;
}
while (--n > 0);
vs2 = AdvSimd.ShiftLeftLogical(vs2, 5);
// Multiply-add bytes by [ 32, 31, 30, ... ] for s2.
vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum1.GetLower(), Vector64.Create((ushort)32, 31, 30, 29));
vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum1.GetUpper(), Vector64.Create((ushort)28, 27, 26, 25));
vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum2.GetLower(), Vector64.Create((ushort)24, 23, 22, 21));
vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum2.GetUpper(), Vector64.Create((ushort)20, 19, 18, 17));
vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum3.GetLower(), Vector64.Create((ushort)16, 15, 14, 13));
vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum3.GetUpper(), Vector64.Create((ushort)12, 11, 10, 9));
vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum4.GetLower(), Vector64.Create((ushort)8, 7, 6, 5));
vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum4.GetUpper(), Vector64.Create((ushort)4, 3, 2, 1));
// Sum epi32 ints v_s1(s2) and accumulate in s1(s2).
Vector64<uint> sum1 = AdvSimd.AddPairwise(vs1.GetLower(), vs1.GetUpper());
Vector64<uint> sum2 = AdvSimd.AddPairwise(vs2.GetLower(), vs2.GetUpper());
Vector64<uint> s1s2 = AdvSimd.AddPairwise(sum1, sum2);
// Store the results.
s1 += AdvSimd.Extract(s1s2, 0);
s2 += AdvSimd.Extract(s1s2, 1);
// Reduce.
s1 %= BASE;
s2 %= BASE;
}
if (length > 0)
{
HandleLeftOver(localBufferPtr, length, ref s1, ref s2);
}
return s1 | (s2 << 16);
}
}
private static unsafe void HandleLeftOver(byte* localBufferPtr, uint length, ref uint s1, ref uint s2)
{
if (length >= 16)
{
s2 += s1 += localBufferPtr[0];
s2 += s1 += localBufferPtr[1];
s2 += s1 += localBufferPtr[2];
s2 += s1 += localBufferPtr[3];
s2 += s1 += localBufferPtr[4];
s2 += s1 += localBufferPtr[5];
s2 += s1 += localBufferPtr[6];
s2 += s1 += localBufferPtr[7];
s2 += s1 += localBufferPtr[8];
s2 += s1 += localBufferPtr[9];
s2 += s1 += localBufferPtr[10];
s2 += s1 += localBufferPtr[11];
s2 += s1 += localBufferPtr[12];
s2 += s1 += localBufferPtr[13];
s2 += s1 += localBufferPtr[14];
s2 += s1 += localBufferPtr[15];
localBufferPtr += 16;
length -= 16;
}
while (length-- > 0)
{
s2 += s1 += *localBufferPtr++;
}
if (s1 >= BASE)
{
s1 -= BASE;
}
s2 %= BASE;
}
[MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)]
private static unsafe uint CalculateScalar(uint adler, ReadOnlySpan<byte> buffer)
{
uint s1 = adler & 0xFFFF;
uint s2 = (adler >> 16) & 0xFFFF;
fixed (byte* bufferPtr = buffer)
{
byte* localBufferPtr = bufferPtr;
uint length = (uint)buffer.Length;
while (length > 0)
{
uint k = length < NMAX ? length : NMAX;
length -= k;
while (k >= 16)
{
s2 += s1 += localBufferPtr[0];
s2 += s1 += localBufferPtr[1];
s2 += s1 += localBufferPtr[2];
s2 += s1 += localBufferPtr[3];
s2 += s1 += localBufferPtr[4];
s2 += s1 += localBufferPtr[5];
s2 += s1 += localBufferPtr[6];
s2 += s1 += localBufferPtr[7];
s2 += s1 += localBufferPtr[8];
s2 += s1 += localBufferPtr[9];
s2 += s1 += localBufferPtr[10];
s2 += s1 += localBufferPtr[11];
s2 += s1 += localBufferPtr[12];
s2 += s1 += localBufferPtr[13];
s2 += s1 += localBufferPtr[14];
s2 += s1 += localBufferPtr[15];
localBufferPtr += 16;
k -= 16;
}
while (k-- > 0)
{
s2 += s1 += *localBufferPtr++;
}
s1 %= BASE;
s2 %= BASE;
}
return (s2 << 16) | s1;
}
}
}

120
src/ImageSharp/Compression/Zlib/ChunkedReadStream.cs

@ -0,0 +1,120 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.IO;
namespace SixLabors.ImageSharp.Compression.Zlib;
/// <summary>
/// A read-only stream over a sequence of length-delimited segments. Bytes are
/// pulled from the inner stream up to the current segment's remaining length;
/// when the segment is exhausted the supplied delegate is invoked to advance
/// to the next segment and return its length. The inner stream is not owned
/// and is not disposed.
/// </summary>
internal sealed class ChunkedReadStream : Stream
{
private static readonly Func<int> GetDataNoOp = () => 0;
private readonly BufferedReadStream innerStream;
private readonly Func<int> getData;
private int currentDataRemaining;
public ChunkedReadStream(BufferedReadStream innerStream)
: this(innerStream, GetDataNoOp)
{
}
public ChunkedReadStream(BufferedReadStream innerStream, Func<int> getData)
{
this.innerStream = innerStream;
this.getData = getData;
}
/// <inheritdoc/>
public override bool CanRead => this.innerStream.CanRead;
/// <inheritdoc/>
public override bool CanSeek => false;
/// <inheritdoc/>
public override bool CanWrite => throw new NotSupportedException();
/// <inheritdoc/>
public override long Length => throw new NotSupportedException();
/// <inheritdoc/>
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
/// <summary>
/// Sets the number of bytes available to read from the current segment.
/// Must be called before reading each segment.
/// </summary>
public void SetCurrentSegmentLength(int bytes) => this.currentDataRemaining = bytes;
/// <inheritdoc/>
public override void Flush() => throw new NotSupportedException();
/// <inheritdoc/>
public override int ReadByte()
{
if (this.currentDataRemaining is 0)
{
this.currentDataRemaining = this.getData();
if (this.currentDataRemaining is 0)
{
return -1;
}
}
int value = this.innerStream.ReadByte();
if (value is not -1)
{
this.currentDataRemaining--;
}
return value;
}
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count) => this.Read(buffer.AsSpan(offset, count));
/// <inheritdoc/>
public override int Read(Span<byte> buffer)
{
// Decrement currentDataRemaining only by bytes actually returned by innerStream.Read; a short read otherwise advances segments too early.
int totalBytesRead = 0;
while (totalBytesRead < buffer.Length)
{
if (this.currentDataRemaining is 0)
{
this.currentDataRemaining = this.getData();
if (this.currentDataRemaining is 0)
{
break;
}
}
int bytesToRead = Math.Min(buffer.Length - totalBytesRead, this.currentDataRemaining);
int bytesRead = this.innerStream.Read(buffer.Slice(totalBytesRead, bytesToRead));
if (bytesRead is 0)
{
break;
}
this.currentDataRemaining -= bytesRead;
totalBytesRead += bytesRead;
}
return totalBytesRead;
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
/// <inheritdoc/>
public override void SetLength(long value) => throw new NotSupportedException();
/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}

143
src/ImageSharp/Compression/Zlib/ChunkedWriteStream.cs

@ -0,0 +1,143 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Compression.Zlib;
/// <summary>
/// A write-only stream that groups written bytes into fixed-length segments. Bytes are
/// collected in a pooled segment buffer; when the buffer is full the supplied delegate is
/// invoked with the completed segment and the buffer is reused. The final partial segment,
/// if any, is emitted on disposal. The delegate owns the destination; this stream writes
/// nowhere itself and is the write-side counterpart of <see cref="ChunkedReadStream"/>.
/// </summary>
internal sealed class ChunkedWriteStream : Stream
{
/// <summary>
/// The segment length used when the caller does not require a specific framing size.
/// </summary>
public const int DefaultSegmentLength = 64 * 1024;
private readonly IMemoryOwner<byte> segmentOwner;
private readonly Memory<byte> segment;
private readonly Action<ReadOnlySpan<byte>> writeSegment;
private int segmentFilled;
private bool isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="ChunkedWriteStream"/> class using <see cref="DefaultSegmentLength"/>.
/// </summary>
/// <param name="allocator">The memory allocator used to rent the segment buffer.</param>
/// <param name="writeSegment">Invoked with each completed segment, and with the final partial segment on disposal.</param>
public ChunkedWriteStream(MemoryAllocator allocator, Action<ReadOnlySpan<byte>> writeSegment)
: this(allocator, DefaultSegmentLength, writeSegment)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChunkedWriteStream"/> class.
/// </summary>
/// <param name="allocator">The memory allocator used to rent the segment buffer.</param>
/// <param name="segmentLength">The length of each completed segment.</param>
/// <param name="writeSegment">Invoked with each completed segment, and with the final partial segment on disposal.</param>
public ChunkedWriteStream(MemoryAllocator allocator, int segmentLength, Action<ReadOnlySpan<byte>> writeSegment)
{
this.segmentOwner = allocator.Allocate<byte>(segmentLength);
this.segment = this.segmentOwner.Memory;
this.writeSegment = writeSegment;
}
/// <inheritdoc/>
public override bool CanRead => false;
/// <inheritdoc/>
public override bool CanSeek => false;
/// <inheritdoc/>
public override bool CanWrite => true;
/// <inheritdoc/>
public override long Length => throw new NotSupportedException();
/// <inheritdoc/>
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
/// <summary>
/// Does nothing. A segment is emitted only when it is full or on disposal, so the segment
/// length stays fixed however often the producer flushes.
/// </summary>
public override void Flush()
{
}
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
/// <inheritdoc/>
public override void SetLength(long value) => throw new NotSupportedException();
/// <inheritdoc/>
public override void WriteByte(byte value)
{
this.segment.Span[this.segmentFilled++] = value;
this.EmitIfFull();
}
/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count) => this.Write(buffer.AsSpan(offset, count));
/// <inheritdoc/>
public override void Write(ReadOnlySpan<byte> buffer)
{
Span<byte> segment = this.segment.Span;
while (!buffer.IsEmpty)
{
int count = Math.Min(segment.Length - this.segmentFilled, buffer.Length);
buffer[..count].CopyTo(segment[this.segmentFilled..]);
this.segmentFilled += count;
buffer = buffer[count..];
this.EmitIfFull();
}
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (this.isDisposed)
{
return;
}
this.isDisposed = true;
if (disposing)
{
// The producer has finished, so the partial segment is the final one.
if (this.segmentFilled > 0)
{
this.writeSegment(this.segment.Span[..this.segmentFilled]);
this.segmentFilled = 0;
}
this.segmentOwner.Dispose();
}
base.Dispose(disposing);
}
/// <summary>
/// Emits the segment buffer when it is full and resets it for reuse.
/// </summary>
private void EmitIfFull()
{
if (this.segmentFilled == this.segment.Length)
{
this.writeSegment(this.segment.Span);
this.segmentFilled = 0;
}
}
}

33
src/ImageSharp/Compression/Zlib/DeflateThrowHelper.cs

@ -1,33 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
namespace SixLabors.ImageSharp.Compression.Zlib;
internal static class DeflateThrowHelper
{
[DoesNotReturn]
public static void ThrowAlreadyFinished() => throw new InvalidOperationException("Finish() already called.");
[DoesNotReturn]
public static void ThrowAlreadyClosed() => throw new InvalidOperationException("Deflator already closed.");
[DoesNotReturn]
public static void ThrowUnknownCompression() => throw new InvalidOperationException("Unknown compression function.");
[DoesNotReturn]
public static void ThrowNotProcessed() => throw new InvalidOperationException("Old input was not completely processed.");
[DoesNotReturn]
public static void ThrowNull(string name) => throw new ArgumentNullException(name);
[DoesNotReturn]
public static void ThrowOutOfRange(string name) => throw new ArgumentOutOfRangeException(name);
[DoesNotReturn]
public static void ThrowHeapViolated() => throw new InvalidOperationException("Huffman heap invariant violated.");
[DoesNotReturn]
public static void ThrowNoDeflate() => throw new ImageFormatException("Cannot deflate all input.");
}

290
src/ImageSharp/Compression/Zlib/Deflater.cs

@ -1,290 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Compression.Zlib;
/// <summary>
/// This class compresses input with the deflate algorithm described in RFC 1951.
/// It has several compression levels and three different strategies described below.
/// </summary>
internal sealed class Deflater : IDisposable
{
/// <summary>
/// The best and slowest compression level. This tries to find very
/// long and distant string repetitions.
/// </summary>
public const int BestCompression = 9;
/// <summary>
/// The worst but fastest compression level.
/// </summary>
public const int BestSpeed = 1;
/// <summary>
/// The default compression level.
/// </summary>
public const int DefaultCompression = -1;
/// <summary>
/// This level won't compress at all but output uncompressed blocks.
/// </summary>
public const int NoCompression = 0;
/// <summary>
/// The compression method. This is the only method supported so far.
/// There is no need to use this constant at all.
/// </summary>
public const int Deflated = 8;
/// <summary>
/// Compression level.
/// </summary>
private int level;
/// <summary>
/// The current state.
/// </summary>
private int state;
private DeflaterEngine engine;
private bool isDisposed;
private const int IsFlushing = 0x04;
private const int IsFinishing = 0x08;
private const int BusyState = 0x10;
private const int FlushingState = 0x14;
private const int FinishingState = 0x1c;
private const int FinishedState = 0x1e;
private const int ClosedState = 0x7f;
/// <summary>
/// Initializes a new instance of the <see cref="Deflater"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
/// <param name="level">The compression level, a value between NoCompression and BestCompression.
/// </param>
/// <exception cref="ArgumentOutOfRangeException">if level is out of range.</exception>
public Deflater(MemoryAllocator memoryAllocator, int level)
{
if (level == DefaultCompression)
{
level = 6;
}
else if (level < NoCompression || level > BestCompression)
{
throw new ArgumentOutOfRangeException(nameof(level));
}
// TODO: Possibly provide DeflateStrategy as an option.
this.engine = new DeflaterEngine(memoryAllocator, DeflateStrategy.Default);
this.SetLevel(level);
this.Reset();
}
/// <summary>
/// Compression Level as an enum for safer use
/// </summary>
public enum CompressionLevel
{
/// <summary>
/// The best and slowest compression level. This tries to find very
/// long and distant string repetitions.
/// </summary>
BestCompression = Deflater.BestCompression,
/// <summary>
/// The worst but fastest compression level.
/// </summary>
BestSpeed = Deflater.BestSpeed,
/// <summary>
/// The default compression level.
/// </summary>
DefaultCompression = Deflater.DefaultCompression,
/// <summary>
/// This level won't compress at all but output uncompressed blocks.
/// </summary>
NoCompression = Deflater.NoCompression,
/// <summary>
/// The compression method. This is the only method supported so far.
/// There is no need to use this constant at all.
/// </summary>
Deflated = Deflater.Deflated
}
/// <summary>
/// Gets a value indicating whetherthe stream was finished and no more output bytes
/// are available.
/// </summary>
public bool IsFinished => (this.state == FinishedState) && this.engine.Pending.IsFlushed;
/// <summary>
/// Gets a value indicating whether the input buffer is empty.
/// You should then call setInput().
/// NOTE: This method can also return true when the stream
/// was finished.
/// </summary>
public bool IsNeedingInput => this.engine.NeedsInput();
/// <summary>
/// Resets the deflater. The deflater acts afterwards as if it was
/// just created with the same compression level and strategy as it
/// had before.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public void Reset()
{
this.state = BusyState;
this.engine.Pending.Reset();
this.engine.Reset();
}
/// <summary>
/// Flushes the current input block. Further calls to Deflate() will
/// produce enough output to inflate everything in the current input
/// block. It is used by DeflaterOutputStream to implement Flush().
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public void Flush() => this.state |= IsFlushing;
/// <summary>
/// Finishes the deflater with the current input block. It is an error
/// to give more input after this method was called. This method must
/// be called to force all bytes to be flushed.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public void Finish() => this.state |= IsFlushing | IsFinishing;
/// <summary>
/// Sets the data which should be compressed next. This should be
/// only called when needsInput indicates that more input is needed.
/// The given byte array should not be changed, before needsInput() returns
/// true again.
/// </summary>
/// <param name="input">The buffer containing the input data.</param>
/// <param name="offset">The start of the data.</param>
/// <param name="count">The number of data bytes of input.</param>
/// <exception cref="InvalidOperationException">
/// if the buffer was finished or if previous input is still pending.
/// </exception>
[MethodImpl(InliningOptions.ShortMethod)]
public void SetInput(byte[] input, int offset, int count)
{
if ((this.state & IsFinishing) != 0)
{
DeflateThrowHelper.ThrowAlreadyFinished();
}
this.engine.SetInput(input, offset, count);
}
/// <summary>
/// Sets the compression level. There is no guarantee of the exact
/// position of the change, but if you call this when needsInput is
/// true the change of compression level will occur somewhere near
/// before the end of the so far given input.
/// </summary>
/// <param name="level">
/// the new compression level.
/// </param>
public void SetLevel(int level)
{
if (level == DefaultCompression)
{
level = 6;
}
else if (level < NoCompression || level > BestCompression)
{
throw new ArgumentOutOfRangeException(nameof(level));
}
if (this.level != level)
{
this.level = level;
this.engine.SetLevel(level);
}
}
/// <summary>
/// Deflates the current input block to the given array.
/// </summary>
/// <param name="output">Buffer to store the compressed data.</param>
/// <param name="offset">Offset into the output array.</param>
/// <param name="length">The maximum number of bytes that may be stored.</param>
/// <returns>
/// The number of compressed bytes added to the output, or 0 if either
/// <see cref="IsNeedingInput"/> or <see cref="IsFinished"/> returns true or length is zero.
/// </returns>
public int Deflate(Span<byte> output, int offset, int length)
{
int origLength = length;
if (this.state == ClosedState)
{
DeflateThrowHelper.ThrowAlreadyClosed();
}
while (true)
{
int count = this.engine.Pending.Flush(output, offset, length);
offset += count;
length -= count;
if (length == 0 || this.state == FinishedState)
{
break;
}
if (!this.engine.Deflate((this.state & IsFlushing) != 0, (this.state & IsFinishing) != 0))
{
switch (this.state)
{
case BusyState:
// We need more input now
return origLength - length;
case FlushingState:
if (this.level != NoCompression)
{
// We have to supply some lookahead. 8 bit lookahead
// is needed by the zlib inflater, and we must fill
// the next byte, so that all bits are flushed.
int neededbits = 8 + ((-this.engine.Pending.BitCount) & 7);
while (neededbits > 0)
{
// Write a static tree block consisting solely of an EOF:
this.engine.Pending.WriteBits(2, 10);
neededbits -= 10;
}
}
this.state = BusyState;
break;
case FinishingState:
this.engine.Pending.AlignToByte();
this.state = FinishedState;
break;
}
}
}
return origLength - length;
}
/// <inheritdoc/>
public void Dispose()
{
if (!this.isDisposed)
{
this.engine.Dispose();
this.isDisposed = true;
}
}
}

148
src/ImageSharp/Compression/Zlib/DeflaterConstants.cs

@ -1,148 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
// <auto-generated/>
using System;
namespace SixLabors.ImageSharp.Compression.Zlib;
/// <summary>
/// This class contains constants used for deflation.
/// </summary>
internal static class DeflaterConstants
{
/// <summary>
/// Set to true to enable debugging
/// </summary>
public const bool DEBUGGING = false;
/// <summary>
/// Written to Zip file to identify a stored block
/// </summary>
public const int STORED_BLOCK = 0;
/// <summary>
/// Identifies static tree in Zip file
/// </summary>
public const int STATIC_TREES = 1;
/// <summary>
/// Identifies dynamic tree in Zip file
/// </summary>
public const int DYN_TREES = 2;
/// <summary>
/// Header flag indicating a preset dictionary for deflation
/// </summary>
public const int PRESET_DICT = 0x20;
/// <summary>
/// Sets internal buffer sizes for Huffman encoding
/// </summary>
public const int DEFAULT_MEM_LEVEL = 8;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int MAX_MATCH = 258;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int MIN_MATCH = 3;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int MAX_WBITS = 15;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int WSIZE = 1 << MAX_WBITS;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int WMASK = WSIZE - 1;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int HASH_BITS = DEFAULT_MEM_LEVEL + 7;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int HASH_SIZE = 1 << HASH_BITS;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int HASH_MASK = HASH_SIZE - 1;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int HASH_SHIFT = (HASH_BITS + MIN_MATCH - 1) / MIN_MATCH;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int MIN_LOOKAHEAD = MAX_MATCH + MIN_MATCH + 1;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int MAX_DIST = WSIZE - MIN_LOOKAHEAD;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int PENDING_BUF_SIZE = 1 << (DEFAULT_MEM_LEVEL + 8);
/// <summary>
/// Internal compression engine constant
/// </summary>
public static int MAX_BLOCK_SIZE = Math.Min(65535, PENDING_BUF_SIZE - 5);
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int DEFLATE_STORED = 0;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int DEFLATE_FAST = 1;
/// <summary>
/// Internal compression engine constant
/// </summary>
public const int DEFLATE_SLOW = 2;
/// <summary>
/// Internal compression engine constant
/// </summary>
public static int[] GOOD_LENGTH = [0, 4, 4, 4, 4, 8, 8, 8, 32, 32];
/// <summary>
/// Internal compression engine constant
/// </summary>
public static int[] MAX_LAZY = [0, 4, 5, 6, 4, 16, 16, 32, 128, 258];
/// <summary>
/// Internal compression engine constant
/// </summary>
public static int[] NICE_LENGTH = [0, 8, 16, 32, 16, 32, 128, 128, 258, 258];
/// <summary>
/// Internal compression engine constant
/// </summary>
public static int[] MAX_CHAIN = [0, 4, 8, 32, 16, 32, 128, 256, 1024, 4096];
/// <summary>
/// Internal compression engine constant
/// </summary>
public static int[] COMPR_FUNC = [0, 1, 1, 1, 1, 2, 2, 2, 2, 2];
}

867
src/ImageSharp/Compression/Zlib/DeflaterEngine.cs

@ -1,867 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Compression.Zlib;
/// <summary>
/// Strategies for deflater
/// </summary>
internal enum DeflateStrategy
{
/// <summary>
/// The default strategy
/// </summary>
Default = 0,
/// <summary>
/// This strategy will only allow longer string repetitions. It is
/// useful for random data with a small character set.
/// </summary>
Filtered = 1,
/// <summary>
/// This strategy will not look for string repetitions at all. It
/// only encodes with Huffman trees (which means, that more common
/// characters get a smaller encoding.
/// </summary>
HuffmanOnly = 2
}
// DEFLATE ALGORITHM:
//
// The uncompressed stream is inserted into the window array. When
// the window array is full the first half is thrown away and the
// second half is copied to the beginning.
//
// The head array is a hash table. Three characters build a hash value
// and they the value points to the corresponding index in window of
// the last string with this hash. The prev array implements a
// linked list of matches with the same hash: prev[index & WMASK] points
// to the previous index with the same hash.
//
/// <summary>
/// Low level compression engine for deflate algorithm which uses a 32K sliding window
/// with secondary compression from Huffman/Shannon-Fano codes.
/// </summary>
internal sealed unsafe class DeflaterEngine : IDisposable
{
private const int TooFar = 4096;
// Hash index of string to be inserted
private int insertHashIndex;
private int matchStart;
// Length of best match
private int matchLen;
// Set if previous match exists
private bool prevAvailable;
private int blockStart;
/// <summary>
/// Points to the current character in the window.
/// </summary>
private int strstart;
/// <summary>
/// lookahead is the number of characters starting at strstart in
/// window that are valid.
/// So window[strstart] until window[strstart+lookahead-1] are valid
/// characters.
/// </summary>
private int lookahead;
/// <summary>
/// The current compression function.
/// </summary>
private int compressionFunction;
/// <summary>
/// The input data for compression.
/// </summary>
private byte[]? inputBuf;
/// <summary>
/// The offset into inputBuf, where input data starts.
/// </summary>
private int inputOff;
/// <summary>
/// The end offset of the input data.
/// </summary>
private int inputEnd;
private readonly DeflateStrategy strategy;
private DeflaterHuffman huffman;
private bool isDisposed;
/// <summary>
/// Hashtable, hashing three characters to an index for window, so
/// that window[index]..window[index+2] have this hash code.
/// Note that the array should really be unsigned short, so you need
/// to and the values with 0xFFFF.
/// </summary>
private IMemoryOwner<short> headMemoryOwner;
private MemoryHandle headMemoryHandle;
private readonly Memory<short> head;
private readonly short* pinnedHeadPointer;
/// <summary>
/// <code>prev[index &amp; WMASK]</code> points to the previous index that has the
/// same hash code as the string starting at index. This way
/// entries with the same hash code are in a linked list.
/// Note that the array should really be unsigned short, so you need
/// to and the values with 0xFFFF.
/// </summary>
private IMemoryOwner<short> prevMemoryOwner;
private MemoryHandle prevMemoryHandle;
private readonly Memory<short> prev;
private readonly short* pinnedPrevPointer;
/// <summary>
/// This array contains the part of the uncompressed stream that
/// is of relevance. The current character is indexed by strstart.
/// </summary>
private IMemoryOwner<byte> windowMemoryOwner;
private MemoryHandle windowMemoryHandle;
private readonly Memory<byte> window;
private readonly byte* pinnedWindowPointer;
private int maxChain;
private int maxLazy;
private int niceLength;
private int goodLength;
/// <summary>
/// Initializes a new instance of the <see cref="DeflaterEngine"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
/// <param name="strategy">The deflate strategy to use.</param>
public DeflaterEngine(MemoryAllocator memoryAllocator, DeflateStrategy strategy)
{
this.huffman = new DeflaterHuffman(memoryAllocator);
this.Pending = this.huffman.Pending;
this.strategy = strategy;
// Create pinned pointers to the various buffers to allow indexing
// without bounds checks.
this.windowMemoryOwner = memoryAllocator.Allocate<byte>(2 * DeflaterConstants.WSIZE);
this.window = this.windowMemoryOwner.Memory;
this.windowMemoryHandle = this.window.Pin();
this.pinnedWindowPointer = (byte*)this.windowMemoryHandle.Pointer;
this.headMemoryOwner = memoryAllocator.Allocate<short>(DeflaterConstants.HASH_SIZE);
this.head = this.headMemoryOwner.Memory;
this.headMemoryHandle = this.head.Pin();
this.pinnedHeadPointer = (short*)this.headMemoryHandle.Pointer;
this.prevMemoryOwner = memoryAllocator.Allocate<short>(DeflaterConstants.WSIZE);
this.prev = this.prevMemoryOwner.Memory;
this.prevMemoryHandle = this.prev.Pin();
this.pinnedPrevPointer = (short*)this.prevMemoryHandle.Pointer;
// We start at index 1, to avoid an implementation deficiency, that
// we cannot build a repeat pattern at index 0.
this.blockStart = this.strstart = 1;
}
/// <summary>
/// Gets the pending buffer to use.
/// </summary>
public DeflaterPendingBuffer Pending { get; }
/// <summary>
/// Deflate drives actual compression of data
/// </summary>
/// <param name="flush">True to flush input buffers</param>
/// <param name="finish">Finish deflation with the current input.</param>
/// <returns>Returns true if progress has been made.</returns>
public bool Deflate(bool flush, bool finish)
{
bool progress = false;
do
{
this.FillWindow();
bool canFlush = flush && (this.inputOff == this.inputEnd);
switch (this.compressionFunction)
{
case DeflaterConstants.DEFLATE_STORED:
progress = this.DeflateStored(canFlush, finish);
break;
case DeflaterConstants.DEFLATE_FAST:
progress = this.DeflateFast(canFlush, finish);
break;
case DeflaterConstants.DEFLATE_SLOW:
progress = this.DeflateSlow(canFlush, finish);
break;
default:
DeflateThrowHelper.ThrowUnknownCompression();
break;
}
}
while (this.Pending.IsFlushed && progress); // repeat while we have no pending output and progress was made
return progress;
}
/// <summary>
/// Sets input data to be deflated. Should only be called when <see cref="NeedsInput"/>
/// returns true
/// </summary>
/// <param name="buffer">The buffer containing input data.</param>
/// <param name="offset">The offset of the first byte of data.</param>
/// <param name="count">The number of bytes of data to use as input.</param>
public void SetInput(byte[]? buffer, int offset, int count)
{
if (buffer is null)
{
DeflateThrowHelper.ThrowNull(nameof(buffer));
}
if (offset < 0)
{
DeflateThrowHelper.ThrowOutOfRange(nameof(offset));
}
if (count < 0)
{
DeflateThrowHelper.ThrowOutOfRange(nameof(count));
}
if (this.inputOff < this.inputEnd)
{
DeflateThrowHelper.ThrowNotProcessed();
}
int end = offset + count;
// We want to throw an ArgumentOutOfRangeException early.
// The check is very tricky: it also handles integer wrap around.
if ((offset > end) || (end > buffer.Length))
{
DeflateThrowHelper.ThrowOutOfRange(nameof(count));
}
this.inputBuf = buffer;
this.inputOff = offset;
this.inputEnd = end;
}
/// <summary>
/// Determines if more <see cref="SetInput">input</see> is needed.
/// </summary>
/// <returns>Return true if input is needed via <see cref="SetInput">SetInput</see></returns>
[MethodImpl(InliningOptions.ShortMethod)]
public bool NeedsInput() => this.inputEnd == this.inputOff;
/// <summary>
/// Reset internal state
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public void Reset()
{
this.huffman.Reset();
this.blockStart = this.strstart = 1;
this.lookahead = 0;
this.prevAvailable = false;
this.matchLen = DeflaterConstants.MIN_MATCH - 1;
this.head.Span[..DeflaterConstants.HASH_SIZE].Clear();
this.prev.Span[..DeflaterConstants.WSIZE].Clear();
}
/// <summary>
/// Set the deflate level (0-9)
/// </summary>
/// <param name="level">The value to set the level to.</param>
public void SetLevel(int level)
{
if (level is < 0 or > 9)
{
DeflateThrowHelper.ThrowOutOfRange(nameof(level));
}
this.goodLength = DeflaterConstants.GOOD_LENGTH[level];
this.maxLazy = DeflaterConstants.MAX_LAZY[level];
this.niceLength = DeflaterConstants.NICE_LENGTH[level];
this.maxChain = DeflaterConstants.MAX_CHAIN[level];
if (DeflaterConstants.COMPR_FUNC[level] != this.compressionFunction)
{
switch (this.compressionFunction)
{
case DeflaterConstants.DEFLATE_STORED:
if (this.strstart > this.blockStart)
{
this.huffman.FlushStoredBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, false);
this.blockStart = this.strstart;
}
this.UpdateHash();
break;
case DeflaterConstants.DEFLATE_FAST:
if (this.strstart > this.blockStart)
{
this.huffman.FlushBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, false);
this.blockStart = this.strstart;
}
break;
case DeflaterConstants.DEFLATE_SLOW:
if (this.prevAvailable)
{
this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xFF);
}
if (this.strstart > this.blockStart)
{
this.huffman.FlushBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, false);
this.blockStart = this.strstart;
}
this.prevAvailable = false;
this.matchLen = DeflaterConstants.MIN_MATCH - 1;
break;
}
this.compressionFunction = DeflaterConstants.COMPR_FUNC[level];
}
}
/// <summary>
/// Fill the window
/// </summary>
public void FillWindow()
{
// If the window is almost full and there is insufficient lookahead,
// move the upper half to the lower one to make room in the upper half.
if (this.strstart >= DeflaterConstants.WSIZE + DeflaterConstants.MAX_DIST)
{
this.SlideWindow();
}
// If there is not enough lookahead, but still some input left, read in the input.
if (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && this.inputOff < this.inputEnd)
{
int more = (2 * DeflaterConstants.WSIZE) - this.lookahead - this.strstart;
if (more > this.inputEnd - this.inputOff)
{
more = this.inputEnd - this.inputOff;
}
ArgumentNullException.ThrowIfNull(this.inputBuf);
Unsafe.CopyBlockUnaligned(
ref this.window.Span[this.strstart + this.lookahead],
ref this.inputBuf[this.inputOff],
unchecked((uint)more));
this.inputOff += more;
this.lookahead += more;
}
if (this.lookahead >= DeflaterConstants.MIN_MATCH)
{
this.UpdateHash();
}
}
/// <inheritdoc/>
public void Dispose()
{
if (!this.isDisposed)
{
this.huffman.Dispose();
this.windowMemoryHandle.Dispose();
this.windowMemoryOwner.Dispose();
this.headMemoryHandle.Dispose();
this.headMemoryOwner.Dispose();
this.prevMemoryHandle.Dispose();
this.prevMemoryOwner.Dispose();
this.isDisposed = true;
}
}
[MethodImpl(InliningOptions.ShortMethod)]
private void UpdateHash()
{
byte* pinned = this.pinnedWindowPointer;
this.insertHashIndex = (pinned[this.strstart] << DeflaterConstants.HASH_SHIFT) ^ pinned[this.strstart + 1];
}
/// <summary>
/// Inserts the current string in the head hash and returns the previous
/// value for this hash.
/// </summary>
/// <returns>The previous hash value</returns>
[MethodImpl(InliningOptions.ShortMethod)]
private int InsertString()
{
short match;
int hash = ((this.insertHashIndex << DeflaterConstants.HASH_SHIFT) ^ this.pinnedWindowPointer[this.strstart + (DeflaterConstants.MIN_MATCH - 1)]) & DeflaterConstants.HASH_MASK;
short* pinnedHead = this.pinnedHeadPointer;
this.pinnedPrevPointer[this.strstart & DeflaterConstants.WMASK] = match = pinnedHead[hash];
pinnedHead[hash] = unchecked((short)this.strstart);
this.insertHashIndex = hash;
return match & 0xFFFF;
}
private void SlideWindow()
{
Unsafe.CopyBlockUnaligned(
ref MemoryMarshal.GetReference(this.window.Span),
ref Unsafe.Add(ref MemoryMarshal.GetReference(this.window.Span), DeflaterConstants.WSIZE),
DeflaterConstants.WSIZE);
this.matchStart -= DeflaterConstants.WSIZE;
this.strstart -= DeflaterConstants.WSIZE;
this.blockStart -= DeflaterConstants.WSIZE;
// Slide the hash table (could be avoided with 32 bit values
// at the expense of memory usage).
short* pinnedHead = this.pinnedHeadPointer;
for (int i = 0; i < DeflaterConstants.HASH_SIZE; ++i)
{
int m = pinnedHead[i] & 0xFFFF;
pinnedHead[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0);
}
// Slide the prev table.
short* pinnedPrev = this.pinnedPrevPointer;
for (int i = 0; i < DeflaterConstants.WSIZE; i++)
{
int m = pinnedPrev[i] & 0xFFFF;
pinnedPrev[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0);
}
}
/// <summary>
/// <para>
/// Find the best (longest) string in the window matching the
/// string starting at strstart.
/// </para>
/// <para>
/// Preconditions:
/// <code>
/// strstart + DeflaterConstants.MAX_MATCH &lt;= window.length.</code>
/// </para>
/// </summary>
/// <param name="curMatch">The current match.</param>
/// <returns>True if a match greater than the minimum length is found</returns>
[MethodImpl(InliningOptions.HotPath)]
private bool FindLongestMatch(int curMatch)
{
int match;
int scan = this.strstart;
// scanMax is the highest position that we can look at
int scanMax = scan + Math.Min(DeflaterConstants.MAX_MATCH, this.lookahead) - 1;
int limit = Math.Max(scan - DeflaterConstants.MAX_DIST, 0);
int chainLength = this.maxChain;
int niceLength = Math.Min(this.niceLength, this.lookahead);
int matchStrt = this.matchStart;
int matchLength = this.matchLen;
matchLength = Math.Max(matchLength, DeflaterConstants.MIN_MATCH - 1);
this.matchLen = matchLength;
if (scan > scanMax - matchLength)
{
return false;
}
int scanEndPosition = scan + matchLength;
byte* pinnedWindow = this.pinnedWindowPointer;
int scanStart = this.strstart;
byte scanEnd1 = pinnedWindow[scanEndPosition - 1];
byte scanEnd = pinnedWindow[scanEndPosition];
// Do not waste too much time if we already have a good match:
if (matchLength >= this.goodLength)
{
chainLength >>= 2;
}
short* pinnedPrev = this.pinnedPrevPointer;
do
{
match = curMatch;
scan = scanStart;
int matchEndPosition = match + matchLength;
if (pinnedWindow[matchEndPosition] != scanEnd
|| pinnedWindow[matchEndPosition - 1] != scanEnd1
|| pinnedWindow[match] != pinnedWindow[scan]
|| pinnedWindow[++match] != pinnedWindow[++scan])
{
continue;
}
// scan is set to strstart+1 and the comparison passed, so
// scanMax - scan is the maximum number of bytes we can compare.
// below we compare 8 bytes at a time, so first we compare
// (scanMax - scan) % 8 bytes, so the remainder is a multiple of 8
// n & (8 - 1) == n % 8.
switch ((scanMax - scan) & 7)
{
case 1:
if (pinnedWindow[++scan] == pinnedWindow[++match])
{
break;
}
break;
case 2:
if (pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match])
{
break;
}
break;
case 3:
if (pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match])
{
break;
}
break;
case 4:
if (pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match])
{
break;
}
break;
case 5:
if (pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match])
{
break;
}
break;
case 6:
if (pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match])
{
break;
}
break;
case 7:
if (pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match])
{
break;
}
break;
}
if (pinnedWindow[scan] == pinnedWindow[match])
{
// We check for insufficient lookahead only every 8th comparison;
// the 256th check will be made at strstart + 258 unless lookahead is
// exhausted first.
do
{
if (scan == scanMax)
{
++scan; // advance to first position not matched
++match;
break;
}
}
while (pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]
&& pinnedWindow[++scan] == pinnedWindow[++match]);
}
if (scan - scanStart > matchLength)
{
matchStrt = curMatch;
matchLength = scan - scanStart;
if (matchLength >= niceLength)
{
break;
}
scanEnd1 = pinnedWindow[scan - 1];
scanEnd = pinnedWindow[scan];
}
}
while ((curMatch = pinnedPrev[curMatch & DeflaterConstants.WMASK] & 0xFFFF) > limit && --chainLength != 0);
this.matchStart = matchStrt;
this.matchLen = matchLength;
return matchLength >= DeflaterConstants.MIN_MATCH;
}
private bool DeflateStored(bool flush, bool finish)
{
if (!flush && (this.lookahead == 0))
{
return false;
}
this.strstart += this.lookahead;
this.lookahead = 0;
int storedLength = this.strstart - this.blockStart;
if ((storedLength >= DeflaterConstants.MAX_BLOCK_SIZE) || // Block is full
(this.blockStart < DeflaterConstants.WSIZE && storedLength >= DeflaterConstants.MAX_DIST) || // Block may move out of window
flush)
{
bool lastBlock = finish;
if (storedLength > DeflaterConstants.MAX_BLOCK_SIZE)
{
storedLength = DeflaterConstants.MAX_BLOCK_SIZE;
lastBlock = false;
}
this.huffman.FlushStoredBlock(this.window.Span, this.blockStart, storedLength, lastBlock);
this.blockStart += storedLength;
return !(lastBlock || storedLength == 0);
}
return true;
}
private bool DeflateFast(bool flush, bool finish)
{
if (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush)
{
return false;
}
const int windowLen = (2 * DeflaterConstants.WSIZE) - DeflaterConstants.MIN_LOOKAHEAD;
while (this.lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush)
{
if (this.lookahead == 0)
{
// We are flushing everything
this.huffman.FlushBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, finish);
this.blockStart = this.strstart;
return false;
}
if (this.strstart > windowLen)
{
// slide window, as FindLongestMatch needs this.
// This should only happen when flushing and the window
// is almost full.
this.SlideWindow();
}
int hashHead;
if (this.lookahead >= DeflaterConstants.MIN_MATCH &&
(hashHead = this.InsertString()) != 0 &&
this.strategy != DeflateStrategy.HuffmanOnly &&
this.strstart - hashHead <= DeflaterConstants.MAX_DIST &&
this.FindLongestMatch(hashHead))
{
// longestMatch sets matchStart and matchLen
bool full = this.huffman.TallyDist(this.strstart - this.matchStart, this.matchLen);
this.lookahead -= this.matchLen;
if (this.matchLen <= this.maxLazy && this.lookahead >= DeflaterConstants.MIN_MATCH)
{
while (--this.matchLen > 0)
{
++this.strstart;
this.InsertString();
}
++this.strstart;
}
else
{
this.strstart += this.matchLen;
if (this.lookahead >= DeflaterConstants.MIN_MATCH - 1)
{
this.UpdateHash();
}
}
this.matchLen = DeflaterConstants.MIN_MATCH - 1;
if (!full)
{
continue;
}
}
else
{
// No match found
this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart] & 0xff);
++this.strstart;
--this.lookahead;
}
if (this.huffman.IsFull())
{
bool lastBlock = finish && (this.lookahead == 0);
this.huffman.FlushBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, lastBlock);
this.blockStart = this.strstart;
return !lastBlock;
}
}
return true;
}
private bool DeflateSlow(bool flush, bool finish)
{
if (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush)
{
return false;
}
const int windowLen = (2 * DeflaterConstants.WSIZE) - DeflaterConstants.MIN_LOOKAHEAD;
while (this.lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush)
{
if (this.lookahead == 0)
{
if (this.prevAvailable)
{
this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xff);
}
this.prevAvailable = false;
// We are flushing everything
this.huffman.FlushBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, finish);
this.blockStart = this.strstart;
return false;
}
if (this.strstart >= windowLen)
{
// slide window, as FindLongestMatch needs this.
// This should only happen when flushing and the window
// is almost full.
this.SlideWindow();
}
int prevMatch = this.matchStart;
int prevLen = this.matchLen;
if (this.lookahead >= DeflaterConstants.MIN_MATCH)
{
int hashHead = this.InsertString();
if (this.strategy != DeflateStrategy.HuffmanOnly &&
hashHead != 0 &&
this.strstart - hashHead <= DeflaterConstants.MAX_DIST &&
this.FindLongestMatch(hashHead))
{
// longestMatch sets matchStart and matchLen
// Discard match if too small and too far away
if (this.matchLen <= 5 && (this.strategy == DeflateStrategy.Filtered || (this.matchLen == DeflaterConstants.MIN_MATCH && this.strstart - this.matchStart > TooFar)))
{
this.matchLen = DeflaterConstants.MIN_MATCH - 1;
}
}
}
// previous match was better
if ((prevLen >= DeflaterConstants.MIN_MATCH) && (this.matchLen <= prevLen))
{
this.huffman.TallyDist(this.strstart - 1 - prevMatch, prevLen);
prevLen -= 2;
do
{
this.strstart++;
this.lookahead--;
if (this.lookahead >= DeflaterConstants.MIN_MATCH)
{
this.InsertString();
}
}
while (--prevLen > 0);
this.strstart++;
this.lookahead--;
this.prevAvailable = false;
this.matchLen = DeflaterConstants.MIN_MATCH - 1;
}
else
{
if (this.prevAvailable)
{
this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xff);
}
this.prevAvailable = true;
this.strstart++;
this.lookahead--;
}
if (this.huffman.IsFull())
{
int len = this.strstart - this.blockStart;
if (this.prevAvailable)
{
len--;
}
bool lastBlock = finish && (this.lookahead == 0) && !this.prevAvailable;
this.huffman.FlushBlock(this.window.Span, this.blockStart, len, lastBlock);
this.blockStart += len;
return !lastBlock;
}
}
return true;
}
}

979
src/ImageSharp/Compression/Zlib/DeflaterHuffman.cs

@ -1,979 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Compression.Zlib;
/// <summary>
/// Performs Deflate Huffman encoding.
/// </summary>
internal sealed unsafe class DeflaterHuffman : IDisposable
{
private const int BufferSize = 1 << (DeflaterConstants.DEFAULT_MEM_LEVEL + 6);
// The number of literal codes.
private const int LiteralNumber = 286;
// Number of distance codes
private const int DistanceNumber = 30;
// Number of codes used to transfer bit lengths
private const int BitLengthNumber = 19;
// Repeat previous bit length 3-6 times (2 bits of repeat count)
private const int Repeat3To6 = 16;
// Repeat a zero length 3-10 times (3 bits of repeat count)
private const int Repeat3To10 = 17;
// Repeat a zero length 11-138 times (7 bits of repeat count)
private const int Repeat11To138 = 18;
private const int EofSymbol = 256;
private Tree literalTree;
private Tree distTree;
private Tree blTree;
// Buffer for distances
private readonly IMemoryOwner<short> distanceMemoryOwner;
private readonly short* pinnedDistanceBuffer;
private MemoryHandle distanceBufferHandle;
private readonly IMemoryOwner<short> literalMemoryOwner;
private readonly short* pinnedLiteralBuffer;
private MemoryHandle literalBufferHandle;
private int lastLiteral;
private int extraBits;
private bool isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="DeflaterHuffman"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
public DeflaterHuffman(MemoryAllocator memoryAllocator)
{
this.Pending = new DeflaterPendingBuffer(memoryAllocator);
this.literalTree = new Tree(memoryAllocator, LiteralNumber, 257, 15);
this.distTree = new Tree(memoryAllocator, DistanceNumber, 1, 15);
this.blTree = new Tree(memoryAllocator, BitLengthNumber, 4, 7);
this.distanceMemoryOwner = memoryAllocator.Allocate<short>(BufferSize);
this.distanceBufferHandle = this.distanceMemoryOwner.Memory.Pin();
this.pinnedDistanceBuffer = (short*)this.distanceBufferHandle.Pointer;
this.literalMemoryOwner = memoryAllocator.Allocate<short>(BufferSize);
this.literalBufferHandle = this.literalMemoryOwner.Memory.Pin();
this.pinnedLiteralBuffer = (short*)this.literalBufferHandle.Pointer;
}
#pragma warning disable SA1201 // Elements should appear in the correct order
// See RFC 1951 3.2.6
// Literal codes
private static readonly short[] StaticLCodes =
[
12, 140, 76, 204, 44, 172, 108, 236, 28, 156, 92, 220, 60, 188, 124, 252,
2, 130, 66, 194, 34, 162, 98, 226, 18, 146, 82, 210, 50, 178, 114, 242,
10, 138, 74, 202, 42, 170, 106, 234, 26, 154, 90, 218, 58, 186, 122, 250,
6, 134, 70, 198, 38, 166, 102, 230, 22, 150, 86, 214, 54, 182, 118, 246,
14, 142, 78, 206, 46, 174, 110, 238, 30, 158, 94, 222, 62, 190, 126, 254,
1, 129, 65, 193, 33, 161, 97, 225, 17, 145, 81, 209, 49, 177, 113, 241, 9,
137, 73, 201, 41, 169, 105, 233, 25, 153, 89, 217, 57, 185, 121, 249, 5,
133, 69, 197, 37, 165, 101, 229, 21, 149, 85, 213, 53, 181, 117, 245, 13,
141, 77, 205, 45, 173, 109, 237, 29, 157, 93, 221, 61, 189, 125, 253, 19,
275, 147, 403, 83, 339, 211, 467, 51, 307, 179, 435, 115, 371, 243, 499,
11, 267, 139, 395, 75, 331, 203, 459, 43, 299, 171, 427, 107, 363, 235, 491,
27, 283, 155, 411, 91, 347, 219, 475, 59, 315, 187, 443, 123, 379, 251, 507,
7, 263, 135, 391, 71, 327, 199, 455, 39, 295, 167, 423, 103, 359, 231, 487,
23, 279, 151, 407, 87, 343, 215, 471, 55, 311, 183, 439, 119, 375, 247, 503,
15, 271, 143, 399, 79, 335, 207, 463, 47, 303, 175, 431, 111, 367, 239, 495,
31, 287, 159, 415, 95, 351, 223, 479, 63, 319, 191, 447, 127, 383, 255, 511,
0, 64, 32, 96, 16, 80, 48, 112, 8, 72, 40, 104, 24, 88, 56, 120, 4, 68, 36,
100, 20, 84, 52, 116, 3, 131, 67, 195, 35, 163
];
private static ReadOnlySpan<byte> StaticLLength =>
[
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,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
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
];
// Distance codes and lengths.
private static readonly short[] StaticDCodes =
[
0, 16, 8, 24, 4, 20, 12, 28, 2, 18, 10, 26, 6, 22, 14,
30, 1, 17, 9, 25, 5, 21, 13, 29, 3, 19, 11, 27, 7, 23
];
private static ReadOnlySpan<byte> StaticDLength =>
[
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5
];
#pragma warning restore SA1201 // Elements should appear in the correct order
/// <summary>
/// Gets the lengths of the bit length codes are sent in order of decreasing probability, to avoid transmitting the lengths for unused bit length codes.
/// </summary>
private static ReadOnlySpan<byte> BitLengthOrder =>
[
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15
];
private static ReadOnlySpan<byte> Bit4Reverse =>
[
0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15
];
/// <summary>
/// Gets the pending buffer to use.
/// </summary>
public DeflaterPendingBuffer Pending { get; private set; }
/// <summary>
/// Reset internal state
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public void Reset()
{
this.lastLiteral = 0;
this.extraBits = 0;
this.literalTree.Reset();
this.distTree.Reset();
this.blTree.Reset();
}
/// <summary>
/// Write all trees to pending buffer
/// </summary>
/// <param name="blTreeCodes">The number/rank of treecodes to send.</param>
public void SendAllTrees(int blTreeCodes)
{
this.blTree.BuildCodes();
this.literalTree.BuildCodes();
this.distTree.BuildCodes();
this.Pending.WriteBits(this.literalTree.NumCodes - 257, 5);
this.Pending.WriteBits(this.distTree.NumCodes - 1, 5);
this.Pending.WriteBits(blTreeCodes - 4, 4);
for (int rank = 0; rank < blTreeCodes; rank++)
{
this.Pending.WriteBits(this.blTree.Length[BitLengthOrder[rank]], 3);
}
this.literalTree.WriteTree(this.Pending, this.blTree);
this.distTree.WriteTree(this.Pending, this.blTree);
}
/// <summary>
/// Compress current buffer writing data to pending buffer
/// </summary>
public void CompressBlock()
{
DeflaterPendingBuffer pendingBuffer = this.Pending;
short* pinnedDistance = this.pinnedDistanceBuffer;
short* pinnedLiteral = this.pinnedLiteralBuffer;
for (int i = 0; i < this.lastLiteral; i++)
{
int litlen = pinnedLiteral[i] & 0xFF;
int dist = pinnedDistance[i];
if (dist-- != 0)
{
int lc = Lcode(litlen);
this.literalTree.WriteSymbol(pendingBuffer, lc);
int bits = (int)(((uint)lc - 261) / 4);
if (bits is > 0 and <= 5)
{
this.Pending.WriteBits(litlen & ((1 << bits) - 1), bits);
}
int dc = Dcode(dist);
this.distTree.WriteSymbol(pendingBuffer, dc);
bits = (dc >> 1) - 1;
if (bits > 0)
{
this.Pending.WriteBits(dist & ((1 << bits) - 1), bits);
}
}
else
{
this.literalTree.WriteSymbol(pendingBuffer, litlen);
}
}
this.literalTree.WriteSymbol(pendingBuffer, EofSymbol);
}
/// <summary>
/// Flush block to output with no compression
/// </summary>
/// <param name="stored">Data to write</param>
/// <param name="storedOffset">Index of first byte to write</param>
/// <param name="storedLength">Count of bytes to write</param>
/// <param name="lastBlock">True if this is the last block</param>
[MethodImpl(InliningOptions.ShortMethod)]
public void FlushStoredBlock(ReadOnlySpan<byte> stored, int storedOffset, int storedLength, bool lastBlock)
{
this.Pending.WriteBits((DeflaterConstants.STORED_BLOCK << 1) + (lastBlock ? 1 : 0), 3);
this.Pending.AlignToByte();
this.Pending.WriteShort(storedLength);
this.Pending.WriteShort(~storedLength);
this.Pending.WriteBlock(stored, storedOffset, storedLength);
this.Reset();
}
/// <summary>
/// Flush block to output with compression
/// </summary>
/// <param name="stored">Data to flush</param>
/// <param name="storedOffset">Index of first byte to flush</param>
/// <param name="storedLength">Count of bytes to flush</param>
/// <param name="lastBlock">True if this is the last block</param>
public void FlushBlock(ReadOnlySpan<byte> stored, int storedOffset, int storedLength, bool lastBlock)
{
this.literalTree.Frequencies[EofSymbol]++;
// Build trees
this.literalTree.BuildTree();
this.distTree.BuildTree();
// Calculate bitlen frequency
this.literalTree.CalcBLFreq(this.blTree);
this.distTree.CalcBLFreq(this.blTree);
// Build bitlen tree
this.blTree.BuildTree();
int blTreeCodes = 4;
for (int i = 18; i > blTreeCodes; i--)
{
if (this.blTree.Length[BitLengthOrder[i]] > 0)
{
blTreeCodes = i + 1;
}
}
int opt_len = 14 + (blTreeCodes * 3) + this.blTree.GetEncodedLength()
+ this.literalTree.GetEncodedLength() + this.distTree.GetEncodedLength()
+ this.extraBits;
int static_len = this.extraBits;
ref byte staticLLengthRef = ref MemoryMarshal.GetReference(StaticLLength);
for (nuint i = 0; i < LiteralNumber; i++)
{
static_len += this.literalTree.Frequencies[i] * Unsafe.Add(ref staticLLengthRef, i);
}
ref byte staticDLengthRef = ref MemoryMarshal.GetReference(StaticDLength);
for (nuint i = 0; i < DistanceNumber; i++)
{
static_len += this.distTree.Frequencies[i] * Unsafe.Add(ref staticDLengthRef, i);
}
if (opt_len >= static_len)
{
// Force static trees
opt_len = static_len;
}
if (storedOffset >= 0 && storedLength + 4 < opt_len >> 3)
{
// Store Block
this.FlushStoredBlock(stored, storedOffset, storedLength, lastBlock);
}
else if (opt_len == static_len)
{
// Encode with static tree
this.Pending.WriteBits((DeflaterConstants.STATIC_TREES << 1) + (lastBlock ? 1 : 0), 3);
this.literalTree.SetStaticCodes(StaticLCodes, StaticLLength);
this.distTree.SetStaticCodes(StaticDCodes, StaticDLength);
this.CompressBlock();
this.Reset();
}
else
{
// Encode with dynamic tree
this.Pending.WriteBits((DeflaterConstants.DYN_TREES << 1) + (lastBlock ? 1 : 0), 3);
this.SendAllTrees(blTreeCodes);
this.CompressBlock();
this.Reset();
}
}
/// <summary>
/// Get value indicating if internal buffer is full
/// </summary>
/// <returns>true if buffer is full</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public bool IsFull() => this.lastLiteral >= BufferSize;
/// <summary>
/// Add literal to buffer
/// </summary>
/// <param name="literal">Literal value to add to buffer.</param>
/// <returns>Value indicating internal buffer is full</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public bool TallyLit(int literal)
{
this.pinnedDistanceBuffer[this.lastLiteral] = 0;
this.pinnedLiteralBuffer[this.lastLiteral++] = (byte)literal;
this.literalTree.Frequencies[literal]++;
return this.IsFull();
}
/// <summary>
/// Add distance code and length to literal and distance trees
/// </summary>
/// <param name="distance">Distance code</param>
/// <param name="length">Length</param>
/// <returns>Value indicating if internal buffer is full</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public bool TallyDist(int distance, int length)
{
this.pinnedDistanceBuffer[this.lastLiteral] = (short)distance;
this.pinnedLiteralBuffer[this.lastLiteral++] = (byte)(length - 3);
int lc = Lcode(length - 3);
this.literalTree.Frequencies[lc]++;
if (lc >= 265 && lc < 285)
{
this.extraBits += (int)(((uint)lc - 261) / 4);
}
int dc = Dcode(distance - 1);
this.distTree.Frequencies[dc]++;
if (dc >= 4)
{
this.extraBits += (dc >> 1) - 1;
}
return this.IsFull();
}
/// <summary>
/// Reverse the bits of a 16 bit value.
/// </summary>
/// <param name="toReverse">Value to reverse bits</param>
/// <returns>Value with bits reversed</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static short BitReverse(int toReverse)
{
/* Use unsafe offsetting and manually validate the input index to reduce the
* total number of conditional branches. There are two main cases to test here:
* 1. In the first 3, the input value (or some combination of it) is combined
* with & 0xF, which results in a maximum value of 0xF no matter what the
* input value was. That is 15, which is always in range for the target span.
* As a result, no input validation is needed at all in this case.
* 2. There are two cases where the input value might cause an invalid access:
* when it is either negative, or greater than 15 << 12. We can test both
* conditions in a single pass by casting the input value to uint and right
* shifting it by 12, which also preserves the sign. If it is a negative
* value (2-complement), the test will fail as the uint cast will result
* in a much larger value. If the value was simply too high, the test will
* fail as expected. We can't simply check whether the value is lower than
* 15 << 12, because higher values are acceptable in the first 3 accesses.
* Doing this reduces the total number of index checks from 4 down to just 1. */
int toReverseRightShiftBy12 = toReverse >> 12;
Guard.MustBeLessThanOrEqualTo<uint>((uint)toReverseRightShiftBy12, 15, nameof(toReverse));
ref byte bit4ReverseRef = ref MemoryMarshal.GetReference(Bit4Reverse);
return (short)((Unsafe.Add(ref bit4ReverseRef, (uint)toReverse & 0xF) << 12)
| (Unsafe.Add(ref bit4ReverseRef, (uint)(toReverse >> 4) & 0xF) << 8)
| (Unsafe.Add(ref bit4ReverseRef, (uint)(toReverse >> 8) & 0xF) << 4)
| Unsafe.Add(ref bit4ReverseRef, (uint)toReverseRightShiftBy12));
}
/// <inheritdoc/>
public void Dispose()
{
if (!this.isDisposed)
{
this.Pending.Dispose();
this.distanceBufferHandle.Dispose();
this.distanceMemoryOwner.Dispose();
this.literalBufferHandle.Dispose();
this.literalMemoryOwner.Dispose();
this.literalTree.Dispose();
this.blTree.Dispose();
this.distTree.Dispose();
this.isDisposed = true;
}
}
[MethodImpl(InliningOptions.ShortMethod)]
private static int Lcode(int length)
{
if (length == 255)
{
return 285;
}
int code = 257;
while (length >= 8)
{
code += 4;
length >>= 1;
}
return code + length;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static int Dcode(int distance)
{
int code = 0;
while (distance >= 4)
{
code += 2;
distance >>= 1;
}
return code + distance;
}
private sealed class Tree : IDisposable
{
private readonly int minNumCodes;
private readonly int[] bitLengthCounts;
private readonly int maxLength;
private bool isDisposed;
private readonly int elementCount;
private readonly MemoryAllocator memoryAllocator;
private IMemoryOwner<short> codesMemoryOwner;
private MemoryHandle codesMemoryHandle;
private readonly short* codes;
private IMemoryOwner<short> frequenciesMemoryOwner;
private MemoryHandle frequenciesMemoryHandle;
private IMemoryOwner<byte> lengthsMemoryOwner;
private MemoryHandle lengthsMemoryHandle;
public Tree(MemoryAllocator memoryAllocator, int elements, int minCodes, int maxLength)
{
this.memoryAllocator = memoryAllocator;
this.elementCount = elements;
this.minNumCodes = minCodes;
this.maxLength = maxLength;
this.frequenciesMemoryOwner = memoryAllocator.Allocate<short>(elements);
this.frequenciesMemoryHandle = this.frequenciesMemoryOwner.Memory.Pin();
this.Frequencies = (short*)this.frequenciesMemoryHandle.Pointer;
this.lengthsMemoryOwner = memoryAllocator.Allocate<byte>(elements);
this.lengthsMemoryHandle = this.lengthsMemoryOwner.Memory.Pin();
this.Length = (byte*)this.lengthsMemoryHandle.Pointer;
this.codesMemoryOwner = memoryAllocator.Allocate<short>(elements);
this.codesMemoryHandle = this.codesMemoryOwner.Memory.Pin();
this.codes = (short*)this.codesMemoryHandle.Pointer;
// Maxes out at 15.
this.bitLengthCounts = new int[maxLength];
}
public int NumCodes { get; private set; }
public short* Frequencies { get; }
public byte* Length { get; }
/// <summary>
/// Resets the internal state of the tree
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public void Reset()
{
this.frequenciesMemoryOwner.Memory.Span.Clear();
this.lengthsMemoryOwner.Memory.Span.Clear();
this.codesMemoryOwner.Memory.Span.Clear();
}
[MethodImpl(InliningOptions.ShortMethod)]
public void WriteSymbol(DeflaterPendingBuffer pendingBuffer, int code)
=> pendingBuffer.WriteBits(this.codes[code] & 0xFFFF, this.Length[code]);
/// <summary>
/// Set static codes and length
/// </summary>
/// <param name="staticCodes">new codes</param>
/// <param name="staticLengths">length for new codes</param>
[MethodImpl(InliningOptions.ShortMethod)]
public void SetStaticCodes(ReadOnlySpan<short> staticCodes, ReadOnlySpan<byte> staticLengths)
{
staticCodes.CopyTo(this.codesMemoryOwner.Memory.Span);
staticLengths.CopyTo(this.lengthsMemoryOwner.Memory.Span);
}
/// <summary>
/// Build dynamic codes and lengths
/// </summary>
public void BuildCodes()
{
// Maxes out at 15 * 4
Span<int> nextCode = stackalloc int[this.maxLength];
ref int nextCodeRef = ref MemoryMarshal.GetReference(nextCode);
ref int bitLengthCountsRef = ref MemoryMarshal.GetReference<int>(this.bitLengthCounts);
int code = 0;
for (int bits = 0; bits < this.maxLength; bits++)
{
Unsafe.Add(ref nextCodeRef, (uint)bits) = code;
code += Unsafe.Add(ref bitLengthCountsRef, (uint)bits) << (15 - bits);
}
for (int i = 0; i < this.NumCodes; i++)
{
int bits = this.Length[i];
if (bits > 0)
{
this.codes[i] = BitReverse(Unsafe.Add(ref nextCodeRef, (uint)(bits - 1)));
Unsafe.Add(ref nextCodeRef, (uint)(bits - 1)) += 1 << (16 - bits);
}
}
}
[MethodImpl(InliningOptions.HotPath)]
public void BuildTree()
{
int numSymbols = this.elementCount;
// heap is a priority queue, sorted by frequency, least frequent
// nodes first. The heap is a binary tree, with the property, that
// the parent node is smaller than both child nodes. This assures
// that the smallest node is the first parent.
//
// The binary tree is encoded in an array: 0 is root node and
// the nodes 2*n+1, 2*n+2 are the child nodes of node n.
// Maxes out at 286 * 4 so too large for the stack.
using (IMemoryOwner<int> heapMemoryOwner = this.memoryAllocator.Allocate<int>(numSymbols))
{
ref int heapRef = ref MemoryMarshal.GetReference(heapMemoryOwner.Memory.Span);
int heapLen = 0;
int maxCode = 0;
for (int n = 0; n < numSymbols; n++)
{
int freq = this.Frequencies[n];
if (freq != 0)
{
// Insert n into heap
int pos = heapLen++;
int ppos;
while (pos > 0 && this.Frequencies[Unsafe.Add(ref heapRef, (uint)(ppos = (pos - 1) >> 1))] > freq)
{
Unsafe.Add(ref heapRef, pos) = Unsafe.Add(ref heapRef, (uint)ppos);
pos = ppos;
}
Unsafe.Add(ref heapRef, (uint)pos) = n;
maxCode = n;
}
}
// We could encode a single literal with 0 bits but then we
// don't see the literals. Therefore we force at least two
// literals to avoid this case. We don't care about order in
// this case, both literals get a 1 bit code.
while (heapLen < 2)
{
Unsafe.Add(ref heapRef, (uint)heapLen++) = maxCode < 2 ? ++maxCode : 0;
}
this.NumCodes = Math.Max(maxCode + 1, this.minNumCodes);
int numLeafs = heapLen;
int childrenLength = (4 * heapLen) - 2;
using (IMemoryOwner<int> childrenMemoryOwner = this.memoryAllocator.Allocate<int>(childrenLength))
using (IMemoryOwner<int> valuesMemoryOwner = this.memoryAllocator.Allocate<int>((2 * heapLen) - 1))
{
ref int childrenRef = ref MemoryMarshal.GetReference(childrenMemoryOwner.Memory.Span);
ref int valuesRef = ref MemoryMarshal.GetReference(valuesMemoryOwner.Memory.Span);
int numNodes = numLeafs;
for (nuint i = 0; i < (uint)heapLen; i++)
{
int node = Unsafe.Add(ref heapRef, i);
nuint i2 = 2 * i;
Unsafe.Add(ref childrenRef, i2) = node;
Unsafe.Add(ref childrenRef, i2 + 1) = -1;
Unsafe.Add(ref valuesRef, i) = this.Frequencies[node] << 8;
Unsafe.Add(ref heapRef, i) = (int)i;
}
// Construct the Huffman tree by repeatedly combining the least two
// frequent nodes.
do
{
int first = Unsafe.Add(ref heapRef, 0);
int last = Unsafe.Add(ref heapRef, (uint)--heapLen);
// Propagate the hole to the leafs of the heap
int ppos = 0;
int path = 1;
while (path < heapLen)
{
if (path + 1 < heapLen && Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)path)) > Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)(path + 1))))
{
path++;
}
Unsafe.Add(ref heapRef, (uint)ppos) = Unsafe.Add(ref heapRef, (uint)path);
ppos = path;
path = (path * 2) + 1;
}
// Now propagate the last element down along path. Normally
// it shouldn't go too deep.
int lastVal = Unsafe.Add(ref valuesRef, (uint)last);
while ((path = ppos) > 0
&& Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)(ppos = (path - 1) >> 1))) > lastVal)
{
Unsafe.Add(ref heapRef, (uint)path) = Unsafe.Add(ref heapRef, (uint)ppos);
}
Unsafe.Add(ref heapRef, (uint)path) = last;
int second = Unsafe.Add(ref heapRef, 0);
// Create a new node father of first and second
last = numNodes++;
Unsafe.Add(ref childrenRef, (uint)(2 * last)) = first;
Unsafe.Add(ref childrenRef, (uint)((2 * last) + 1)) = second;
int mindepth = Math.Min(Unsafe.Add(ref valuesRef, (uint)first) & 0xFF, Unsafe.Add(ref valuesRef, (uint)second) & 0xFF);
Unsafe.Add(ref valuesRef, (uint)last) = lastVal = Unsafe.Add(ref valuesRef, (uint)first) + Unsafe.Add(ref valuesRef, (uint)second) - mindepth + 1;
// Again, propagate the hole to the leafs
ppos = 0;
path = 1;
while (path < heapLen)
{
if (path + 1 < heapLen
&& Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)path)) > Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)(path + 1))))
{
path++;
}
Unsafe.Add(ref heapRef, (uint)ppos) = Unsafe.Add(ref heapRef, (uint)path);
ppos = path;
path = (ppos * 2) + 1;
}
// Now propagate the new element down along path
while ((path = ppos) > 0 && Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)(ppos = (path - 1) >> 1))) > lastVal)
{
Unsafe.Add(ref heapRef, (uint)path) = Unsafe.Add(ref heapRef, (uint)ppos);
}
Unsafe.Add(ref heapRef, (uint)path) = last;
}
while (heapLen > 1);
if (Unsafe.Add(ref heapRef, 0) != (childrenLength >> 1) - 1)
{
DeflateThrowHelper.ThrowHeapViolated();
}
this.BuildLength(childrenMemoryOwner.Memory.Span);
}
}
}
/// <summary>
/// Get encoded length
/// </summary>
/// <returns>Encoded length, the sum of frequencies * lengths</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public int GetEncodedLength()
{
int len = 0;
for (int i = 0; i < this.elementCount; i++)
{
len += this.Frequencies[i] * this.Length[i];
}
return len;
}
/// <summary>
/// Scan a literal or distance tree to determine the frequencies of the codes
/// in the bit length tree.
/// </summary>
public void CalcBLFreq(Tree blTree)
{
int maxCount; // max repeat count
int minCount; // min repeat count
int count; // repeat count of the current code
int curLen = -1; // length of current code
int i = 0;
while (i < this.NumCodes)
{
count = 1;
int nextlen = this.Length[i];
if (nextlen == 0)
{
maxCount = 138;
minCount = 3;
}
else
{
maxCount = 6;
minCount = 3;
if (curLen != nextlen)
{
blTree.Frequencies[nextlen]++;
count = 0;
}
}
curLen = nextlen;
i++;
while (i < this.NumCodes && curLen == this.Length[i])
{
i++;
if (++count >= maxCount)
{
break;
}
}
if (count < minCount)
{
blTree.Frequencies[curLen] += (short)count;
}
else if (curLen != 0)
{
blTree.Frequencies[Repeat3To6]++;
}
else if (count <= 10)
{
blTree.Frequencies[Repeat3To10]++;
}
else
{
blTree.Frequencies[Repeat11To138]++;
}
}
}
/// <summary>
/// Write the tree values.
/// </summary>
/// <param name="pendingBuffer">The pending buffer.</param>
/// <param name="bitLengthTree">The tree to write.</param>
public void WriteTree(DeflaterPendingBuffer pendingBuffer, Tree bitLengthTree)
{
int maxCount; // max repeat count
int minCount; // min repeat count
int count; // repeat count of the current code
int curLen = -1; // length of current code
int i = 0;
while (i < this.NumCodes)
{
count = 1;
int nextlen = this.Length[i];
if (nextlen == 0)
{
maxCount = 138;
minCount = 3;
}
else
{
maxCount = 6;
minCount = 3;
if (curLen != nextlen)
{
bitLengthTree.WriteSymbol(pendingBuffer, nextlen);
count = 0;
}
}
curLen = nextlen;
i++;
while (i < this.NumCodes && curLen == this.Length[i])
{
i++;
if (++count >= maxCount)
{
break;
}
}
if (count < minCount)
{
while (count-- > 0)
{
bitLengthTree.WriteSymbol(pendingBuffer, curLen);
}
}
else if (curLen != 0)
{
bitLengthTree.WriteSymbol(pendingBuffer, Repeat3To6);
pendingBuffer.WriteBits(count - 3, 2);
}
else if (count <= 10)
{
bitLengthTree.WriteSymbol(pendingBuffer, Repeat3To10);
pendingBuffer.WriteBits(count - 3, 3);
}
else
{
bitLengthTree.WriteSymbol(pendingBuffer, Repeat11To138);
pendingBuffer.WriteBits(count - 11, 7);
}
}
}
private void BuildLength(ReadOnlySpan<int> children)
{
byte* lengthPtr = this.Length;
ref int childrenRef = ref MemoryMarshal.GetReference(children);
ref int bitLengthCountsRef = ref MemoryMarshal.GetReference<int>(this.bitLengthCounts);
int maxLen = this.maxLength;
int numNodes = children.Length >> 1;
int numLeafs = (numNodes + 1) >> 1;
int overflow = 0;
Array.Clear(this.bitLengthCounts, 0, maxLen);
// First calculate optimal bit lengths
using (IMemoryOwner<int> lengthsMemoryOwner = this.memoryAllocator.Allocate<int>(numNodes, AllocationOptions.Clean))
{
ref int lengthsRef = ref MemoryMarshal.GetReference(lengthsMemoryOwner.Memory.Span);
for (int i = numNodes - 1; i >= 0; i--)
{
if (children[(2 * i) + 1] != -1)
{
int bitLength = Unsafe.Add(ref lengthsRef, (uint)i) + 1;
if (bitLength > maxLen)
{
bitLength = maxLen;
overflow++;
}
Unsafe.Add(ref lengthsRef, (uint)Unsafe.Add(ref childrenRef, (uint)(2 * i))) = Unsafe.Add(ref lengthsRef, (uint)Unsafe.Add(ref childrenRef, (uint)((2 * i) + 1))) = bitLength;
}
else
{
// A leaf node
int bitLength = Unsafe.Add(ref lengthsRef, (uint)i);
Unsafe.Add(ref bitLengthCountsRef, (uint)(bitLength - 1))++;
lengthPtr[Unsafe.Add(ref childrenRef, (uint)(2 * i))] = (byte)Unsafe.Add(ref lengthsRef, (uint)i);
}
}
}
if (overflow == 0)
{
return;
}
int incrBitLen = maxLen - 1;
do
{
// Find the first bit length which could increase:
while (Unsafe.Add(ref bitLengthCountsRef, (uint)--incrBitLen) == 0)
{
}
// Move this node one down and remove a corresponding
// number of overflow nodes.
do
{
Unsafe.Add(ref bitLengthCountsRef, (uint)incrBitLen)--;
Unsafe.Add(ref bitLengthCountsRef, (uint)++incrBitLen)++;
overflow -= 1 << (maxLen - 1 - incrBitLen);
}
while (overflow > 0 && incrBitLen < maxLen - 1);
}
while (overflow > 0);
// We may have overshot above. Move some nodes from maxLength to
// maxLength-1 in that case.
Unsafe.Add(ref bitLengthCountsRef, (uint)(maxLen - 1)) += overflow;
Unsafe.Add(ref bitLengthCountsRef, (uint)(maxLen - 2)) -= overflow;
// Now recompute all bit lengths, scanning in increasing
// frequency. It is simpler to reconstruct all lengths instead of
// fixing only the wrong ones. This idea is taken from 'ar'
// written by Haruhiko Okumura.
//
// The nodes were inserted with decreasing frequency into the childs
// array.
int nodeIndex = 2 * numLeafs;
for (int bits = maxLen; bits != 0; bits--)
{
int n = Unsafe.Add(ref bitLengthCountsRef, (uint)(bits - 1));
while (n > 0)
{
int childIndex = 2 * Unsafe.Add(ref childrenRef, (uint)nodeIndex++);
if (Unsafe.Add(ref childrenRef, (uint)(childIndex + 1)) == -1)
{
// We found another leaf
lengthPtr[Unsafe.Add(ref childrenRef, (uint)childIndex)] = (byte)bits;
n--;
}
}
}
}
public void Dispose()
{
if (!this.isDisposed)
{
this.frequenciesMemoryHandle.Dispose();
this.frequenciesMemoryOwner.Dispose();
this.lengthsMemoryHandle.Dispose();
this.lengthsMemoryOwner.Dispose();
this.codesMemoryHandle.Dispose();
this.codesMemoryOwner.Dispose();
this.isDisposed = true;
}
}
}
}

143
src/ImageSharp/Compression/Zlib/DeflaterOutputStream.cs

@ -1,143 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Compression.Zlib;
/// <summary>
/// A special stream deflating or compressing the bytes that are
/// written to it. It uses a Deflater to perform actual deflating.
/// </summary>
internal sealed class DeflaterOutputStream : Stream
{
private const int BufferLength = 512;
private IMemoryOwner<byte> memoryOwner;
private readonly Memory<byte> buffer;
private Deflater deflater;
private readonly Stream rawStream;
private bool isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="DeflaterOutputStream"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
/// <param name="rawStream">The output stream where deflated output is written.</param>
/// <param name="compressionLevel">The compression level.</param>
public DeflaterOutputStream(MemoryAllocator memoryAllocator, Stream rawStream, int compressionLevel)
{
this.rawStream = rawStream;
this.memoryOwner = memoryAllocator.Allocate<byte>(BufferLength);
this.buffer = this.memoryOwner.Memory;
this.deflater = new Deflater(memoryAllocator, compressionLevel);
}
/// <inheritdoc/>
public override bool CanRead => false;
/// <inheritdoc/>
public override bool CanSeek => false;
/// <inheritdoc/>
public override bool CanWrite => this.rawStream.CanWrite;
/// <inheritdoc/>
public override long Length => this.rawStream.Length;
/// <inheritdoc/>
public override long Position
{
get => this.rawStream.Position;
set => throw new NotSupportedException();
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
/// <inheritdoc/>
public override void SetLength(long value) => throw new NotSupportedException();
/// <inheritdoc/>
public override int ReadByte() => throw new NotSupportedException();
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
/// <inheritdoc/>
public override void Flush()
{
this.deflater.Flush();
this.Deflate(true);
this.rawStream.Flush();
}
/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count)
{
this.deflater.SetInput(buffer, offset, count);
this.Deflate();
}
private void Deflate() => this.Deflate(false);
private void Deflate(bool flushing)
{
while (flushing || !this.deflater.IsNeedingInput)
{
int deflateCount = this.deflater.Deflate(this.buffer.Span, 0, BufferLength);
if (deflateCount <= 0)
{
break;
}
this.rawStream.Write(this.buffer.Span[..deflateCount]);
}
if (!this.deflater.IsNeedingInput)
{
DeflateThrowHelper.ThrowNoDeflate();
}
}
private void Finish()
{
this.deflater.Finish();
while (!this.deflater.IsFinished)
{
int len = this.deflater.Deflate(this.buffer.Span, 0, BufferLength);
if (len <= 0)
{
break;
}
this.rawStream.Write(this.buffer.Span[..len]);
}
if (!this.deflater.IsFinished)
{
DeflateThrowHelper.ThrowNoDeflate();
}
this.rawStream.Flush();
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (!this.isDisposed)
{
if (disposing)
{
this.Finish();
this.deflater.Dispose();
this.memoryOwner.Dispose();
}
this.isDisposed = true;
base.Dispose(disposing);
}
}
}

185
src/ImageSharp/Compression/Zlib/DeflaterPendingBuffer.cs

@ -1,185 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Compression.Zlib;
/// <summary>
/// Stores pending data for writing data to the Deflater.
/// </summary>
internal sealed unsafe class DeflaterPendingBuffer : IDisposable
{
private readonly Memory<byte> buffer;
private readonly byte* pinnedBuffer;
private IMemoryOwner<byte> bufferMemoryOwner;
private MemoryHandle bufferMemoryHandle;
private int start;
private int end;
private uint bits;
private bool isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="DeflaterPendingBuffer"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
public DeflaterPendingBuffer(MemoryAllocator memoryAllocator)
{
this.bufferMemoryOwner = memoryAllocator.Allocate<byte>(DeflaterConstants.PENDING_BUF_SIZE);
this.buffer = this.bufferMemoryOwner.Memory;
this.bufferMemoryHandle = this.buffer.Pin();
this.pinnedBuffer = (byte*)this.bufferMemoryHandle.Pointer;
}
/// <summary>
/// Gets the number of bits written to the buffer.
/// </summary>
public int BitCount { get; private set; }
/// <summary>
/// Gets a value indicating whether indicates the buffer has been flushed.
/// </summary>
public bool IsFlushed => this.end == 0;
/// <summary>
/// Clear internal state/buffers.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public void Reset() => this.start = this.end = this.BitCount = 0;
/// <summary>
/// Write a short value to buffer LSB first.
/// </summary>
/// <param name="value">The value to write.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public void WriteShort(int value)
{
byte* pinned = this.pinnedBuffer;
pinned[this.end++] = unchecked((byte)value);
pinned[this.end++] = unchecked((byte)(value >> 8));
}
/// <summary>
/// Write a block of data to the internal buffer.
/// </summary>
/// <param name="block">The data to write.</param>
/// <param name="offset">The offset of first byte to write.</param>
/// <param name="length">The number of bytes to write.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public void WriteBlock(ReadOnlySpan<byte> block, int offset, int length)
{
Unsafe.CopyBlockUnaligned(
ref this.buffer.Span[this.end],
ref MemoryMarshal.GetReference(block[offset..]),
unchecked((uint)length));
this.end += length;
}
/// <summary>
/// Aligns internal buffer on a byte boundary.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public void AlignToByte()
{
if (this.BitCount > 0)
{
byte* pinned = this.pinnedBuffer;
pinned[this.end++] = unchecked((byte)this.bits);
if (this.BitCount > 8)
{
pinned[this.end++] = unchecked((byte)(this.bits >> 8));
}
}
this.bits = 0;
this.BitCount = 0;
}
/// <summary>
/// Write bits to internal buffer
/// </summary>
/// <param name="b">source of bits</param>
/// <param name="count">number of bits to write</param>
[MethodImpl(InliningOptions.ShortMethod)]
public void WriteBits(int b, int count)
{
this.bits |= (uint)(b << this.BitCount);
this.BitCount += count;
if (this.BitCount >= 16)
{
byte* pinned = this.pinnedBuffer;
pinned[this.end++] = unchecked((byte)this.bits);
pinned[this.end++] = unchecked((byte)(this.bits >> 8));
this.bits >>= 16;
this.BitCount -= 16;
}
}
/// <summary>
/// Write a short value to internal buffer most significant byte first
/// </summary>
/// <param name="value">The value to write</param>
[MethodImpl(InliningOptions.ShortMethod)]
public void WriteShortMSB(int value)
{
byte* pinned = this.pinnedBuffer;
pinned[this.end++] = unchecked((byte)(value >> 8));
pinned[this.end++] = unchecked((byte)value);
}
/// <summary>
/// Flushes the pending buffer into the given output array.
/// If the output array is to small, only a partial flush is done.
/// </summary>
/// <param name="output">The output array.</param>
/// <param name="offset">The offset into output array.</param>
/// <param name="length">The maximum number of bytes to store.</param>
/// <returns>The number of bytes flushed.</returns>
public int Flush(Span<byte> output, int offset, int length)
{
if (this.BitCount >= 8)
{
this.pinnedBuffer[this.end++] = unchecked((byte)this.bits);
this.bits >>= 8;
this.BitCount -= 8;
}
if (length > this.end - this.start)
{
length = this.end - this.start;
Unsafe.CopyBlockUnaligned(
ref output[offset],
ref this.buffer.Span[this.start],
unchecked((uint)length));
this.start = 0;
this.end = 0;
}
else
{
Unsafe.CopyBlockUnaligned(
ref output[offset],
ref this.buffer.Span[this.start],
unchecked((uint)length));
this.start += length;
}
return length;
}
/// <inheritdoc/>
public void Dispose()
{
if (!this.isDisposed)
{
this.bufferMemoryHandle.Dispose();
this.bufferMemoryOwner.Dispose();
this.isDisposed = true;
}
}
}

11
src/ImageSharp/Compression/Zlib/README.md

@ -1,11 +0,0 @@
DeflateStream implementation adapted from
https://github.com/icsharpcode/SharpZipLib
Licensed under MIT
Crc32 and Adler32 SIMD implementation adapted from
https://github.com/chromium/chromium
Licensed under BSD 3-Clause "New" or "Revised" License

177
src/ImageSharp/Compression/Zlib/ZlibDeflateStream.cs

@ -1,177 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Compression.Zlib;
/// <summary>
/// Provides methods and properties for compressing streams by using the Zlib Deflate algorithm.
/// </summary>
internal sealed class ZlibDeflateStream : Stream
{
/// <summary>
/// The raw stream containing the uncompressed image data.
/// </summary>
private readonly Stream rawStream;
/// <summary>
/// Computes the checksum for the data stream.
/// </summary>
private uint adler = Adler32.SeedValue;
/// <summary>
/// A value indicating whether this instance of the given entity has been disposed.
/// </summary>
/// <value><see langword="true"/> if this instance has been disposed; otherwise, <see langword="false"/>.</value>
/// <remarks>
/// If the entity is disposed, it must not be disposed a second
/// time. The isDisposed field is set the first time the entity
/// is disposed. If the isDisposed field is true, then the Dispose()
/// method will not dispose again. This help not to prolong the entity's
/// life in the Garbage Collector.
/// </remarks>
private bool isDisposed;
/// <summary>
/// The stream responsible for compressing the input stream.
/// </summary>
private DeflaterOutputStream deflateStream;
/// <summary>
/// Initializes a new instance of the <see cref="ZlibDeflateStream"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
/// <param name="stream">The stream to compress.</param>
/// <param name="level">The compression level.</param>
public ZlibDeflateStream(MemoryAllocator memoryAllocator, Stream stream, DeflateCompressionLevel level)
: this(memoryAllocator, stream, (PngCompressionLevel)level)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ZlibDeflateStream"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
/// <param name="stream">The stream to compress.</param>
/// <param name="level">The compression level.</param>
public ZlibDeflateStream(MemoryAllocator memoryAllocator, Stream stream, PngCompressionLevel level)
{
int compressionLevel = (int)level;
this.rawStream = stream;
// Write the zlib header : http://tools.ietf.org/html/rfc1950
// CMF(Compression Method and flags)
// This byte is divided into a 4 - bit compression method and a
// 4-bit information field depending on the compression method.
// bits 0 to 3 CM Compression method
// bits 4 to 7 CINFO Compression info
//
// 0 1
// +---+---+
// |CMF|FLG|
// +---+---+
const int Cmf = 0x78;
int flg = 218;
// http://stackoverflow.com/a/2331025/277304
if (compressionLevel >= 5 && compressionLevel <= 6)
{
flg = 156;
}
else if (compressionLevel >= 3 && compressionLevel <= 4)
{
flg = 94;
}
else if (compressionLevel <= 2)
{
flg = 1;
}
// Just in case
flg -= ((Cmf * 256) + flg) % 31;
if (flg < 0)
{
flg += 31;
}
this.rawStream.WriteByte(Cmf);
this.rawStream.WriteByte((byte)flg);
this.deflateStream = new DeflaterOutputStream(memoryAllocator, this.rawStream, compressionLevel);
}
/// <inheritdoc/>
public override bool CanRead => false;
/// <inheritdoc/>
public override bool CanSeek => false;
/// <inheritdoc/>
public override bool CanWrite => this.rawStream.CanWrite;
/// <inheritdoc/>
public override long Length => this.rawStream.Length;
/// <inheritdoc/>
public override long Position
{
get
{
return this.rawStream.Position;
}
set
{
throw new NotSupportedException();
}
}
/// <inheritdoc/>
public override void Flush() => this.deflateStream.Flush();
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
/// <inheritdoc/>
public override void SetLength(long value) => throw new NotSupportedException();
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public override void Write(byte[] buffer, int offset, int count)
{
this.deflateStream.Write(buffer, offset, count);
this.adler = Adler32.Calculate(this.adler, buffer.AsSpan(offset, count));
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (this.isDisposed)
{
return;
}
if (disposing)
{
// dispose managed resources
this.deflateStream.Dispose();
// Add the crc
uint crc = this.adler;
this.rawStream.WriteByte((byte)((crc >> 24) & 0xFF));
this.rawStream.WriteByte((byte)((crc >> 16) & 0xFF));
this.rawStream.WriteByte((byte)((crc >> 8) & 0xFF));
this.rawStream.WriteByte((byte)(crc & 0xFF));
}
base.Dispose(disposing);
this.isDisposed = true;
}
}

120
src/ImageSharp/Compression/Zlib/ZlibInflateReader.cs

@ -0,0 +1,120 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using System.IO.Compression;
using SixLabors.ImageSharp.IO;
namespace SixLabors.ImageSharp.Compression.Zlib;
/// <summary>
/// Reads chunked input, parses the zlib CMF/FLG header, and exposes a
/// <see cref="DeflateStream"/> over the remaining DEFLATE payload. The
/// Adler-32 trailer is not validated.
/// </summary>
internal sealed class ZlibInflateReader : IDisposable
{
private readonly ChunkedReadStream segmentStream;
public ZlibInflateReader(BufferedReadStream innerStream)
=> this.segmentStream = new ChunkedReadStream(innerStream);
public ZlibInflateReader(BufferedReadStream innerStream, Func<int> getData)
=> this.segmentStream = new ChunkedReadStream(innerStream, getData);
/// <summary>
/// Gets the compressed stream over the deframed inner stream.
/// </summary>
public DeflateStream? CompressedStream { get; private set; }
/// <summary>
/// Sets the length of the next segment of compressed input and, on first
/// call, parses the zlib header.
/// </summary>
/// <param name="bytes">The remaining data length for the current segment.</param>
/// <param name="isCriticalChunk">Whether to throw on a malformed zlib header.</param>
/// <returns>The <see cref="bool"/>.</returns>
[MemberNotNullWhen(true, nameof(CompressedStream))]
public bool AllocateNewBytes(int bytes, bool isCriticalChunk)
{
this.segmentStream.SetCurrentSegmentLength(bytes);
if (this.CompressedStream is null)
{
return this.InitializeInflateStream(isCriticalChunk);
}
return true;
}
public void Dispose()
{
this.CompressedStream?.Dispose();
this.segmentStream?.Dispose();
}
[MemberNotNullWhen(true, nameof(CompressedStream))]
private bool InitializeInflateStream(bool isCriticalChunk)
{
// Read the zlib header : http://tools.ietf.org/html/rfc1950
// CMF(Compression Method and flags)
// This byte is divided into a 4 - bit compression method and a
// 4-bit information field depending on the compression method.
// bits 0 to 3 CM Compression method
// bits 4 to 7 CINFO Compression info
//
// 0 1
// +---+---+
// |CMF|FLG|
// +---+---+
int cmf = this.segmentStream.ReadByte();
int flag = this.segmentStream.ReadByte();
if (cmf == -1 || flag == -1)
{
return false;
}
if ((cmf & 0x0F) == 8)
{
// CINFO is the base-2 logarithm of the LZ77 window size, minus eight.
int cinfo = (cmf & 0xF0) >> 4;
if (cinfo > 7)
{
if (isCriticalChunk)
{
// Values of CINFO above 7 are not allowed in RFC1950.
// CINFO is not defined in this specification for CM not equal to 8.
throw new ImageFormatException($"Invalid window size for ZLIB header: cinfo={cinfo}");
}
return false;
}
}
else if (isCriticalChunk)
{
throw new ImageFormatException($"Bad method for ZLIB header: cmf={cmf}");
}
else
{
return false;
}
// The preset dictionary.
bool fdict = (flag & 32) != 0;
if (fdict)
{
// We don't need this for inflate so simply skip by the next four bytes.
// https://tools.ietf.org/html/rfc1950#page-6
InlineArray4<byte> checksumBuffer = default;
if (this.segmentStream.Read(checksumBuffer) != 4)
{
return false;
}
}
this.CompressedStream = new DeflateStream(this.segmentStream, CompressionMode.Decompress, leaveOpen: true);
return true;
}
}

277
src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs

@ -1,277 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using System.IO.Compression;
using SixLabors.ImageSharp.IO;
namespace SixLabors.ImageSharp.Compression.Zlib;
/// <summary>
/// Provides methods and properties for deframing streams from PNGs.
/// </summary>
internal sealed class ZlibInflateStream : Stream
{
/// <summary>
/// Used to read the Adler-32 and Crc-32 checksums.
/// We don't actually use this for anything so it doesn't
/// have to be threadsafe.
/// </summary>
private static readonly byte[] ChecksumBuffer = new byte[4];
/// <summary>
/// A default delegate to get more data from the inner stream.
/// </summary>
private static readonly Func<int> GetDataNoOp = () => 0;
/// <summary>
/// The inner raw memory stream.
/// </summary>
private readonly BufferedReadStream innerStream;
/// <summary>
/// A value indicating whether this instance of the given entity has been disposed.
/// </summary>
/// <value><see langword="true"/> if this instance has been disposed; otherwise, <see langword="false"/>.</value>
/// <remarks>
/// If the entity is disposed, it must not be disposed a second
/// time. The isDisposed field is set the first time the entity
/// is disposed. If the isDisposed field is true, then the Dispose()
/// method will not dispose again. This help not to prolong the entity's
/// life in the Garbage Collector.
/// </remarks>
private bool isDisposed;
/// <summary>
/// The current data remaining to be read.
/// </summary>
private int currentDataRemaining;
/// <summary>
/// Delegate to get more data once we've exhausted the current data remaining.
/// </summary>
private readonly Func<int> getData;
/// <summary>
/// Initializes a new instance of the <see cref="ZlibInflateStream"/> class.
/// </summary>
/// <param name="innerStream">The inner raw stream.</param>
public ZlibInflateStream(BufferedReadStream innerStream)
: this(innerStream, GetDataNoOp)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ZlibInflateStream"/> class.
/// </summary>
/// <param name="innerStream">The inner raw stream.</param>
/// <param name="getData">A delegate to get more data from the inner stream.</param>
public ZlibInflateStream(BufferedReadStream innerStream, Func<int> getData)
{
this.innerStream = innerStream;
this.getData = getData;
}
/// <inheritdoc/>
public override bool CanRead => this.innerStream.CanRead;
/// <inheritdoc/>
public override bool CanSeek => false;
/// <inheritdoc/>
public override bool CanWrite => throw new NotSupportedException();
/// <inheritdoc/>
public override long Length => throw new NotSupportedException();
/// <inheritdoc/>
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
/// <summary>
/// Gets the compressed stream over the deframed inner stream.
/// </summary>
public DeflateStream? CompressedStream { get; private set; }
/// <summary>
/// Adds new bytes from a frame found in the original stream.
/// </summary>
/// <param name="bytes">The current remaining data according to the chunk length.</param>
/// <param name="isCriticalChunk">Whether the chunk to be inflated is a critical chunk.</param>
/// <returns>The <see cref="bool"/>.</returns>
[MemberNotNullWhen(true, nameof(CompressedStream))]
public bool AllocateNewBytes(int bytes, bool isCriticalChunk)
{
this.currentDataRemaining = bytes;
if (this.CompressedStream is null)
{
return this.InitializeInflateStream(isCriticalChunk);
}
return true;
}
/// <inheritdoc/>
public override void Flush() => throw new NotSupportedException();
/// <inheritdoc/>
public override int ReadByte()
{
this.currentDataRemaining--;
return this.innerStream.ReadByte();
}
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
if (this.currentDataRemaining is 0)
{
// Last buffer was read in its entirety, let's make sure we don't actually have more in additional IDAT chunks.
this.currentDataRemaining = this.getData();
if (this.currentDataRemaining is 0)
{
return 0;
}
}
int bytesToRead = Math.Min(count, this.currentDataRemaining);
this.currentDataRemaining -= bytesToRead;
int totalBytesRead = this.innerStream.Read(buffer, offset, bytesToRead);
long innerStreamLength = this.innerStream.Length;
// Keep reading data until we've reached the end of the stream or filled the buffer.
int bytesRead = 0;
offset += totalBytesRead;
while (this.currentDataRemaining is 0 && totalBytesRead < count)
{
this.currentDataRemaining = this.getData();
if (this.currentDataRemaining is 0)
{
return totalBytesRead;
}
offset += bytesRead;
if (offset >= innerStreamLength || offset >= count)
{
return totalBytesRead;
}
bytesToRead = Math.Min(count - totalBytesRead, this.currentDataRemaining);
this.currentDataRemaining -= bytesToRead;
bytesRead = this.innerStream.Read(buffer, offset, bytesToRead);
if (bytesRead == 0)
{
return totalBytesRead;
}
totalBytesRead += bytesRead;
}
return totalBytesRead;
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
/// <inheritdoc/>
public override void SetLength(long value) => throw new NotSupportedException();
/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (this.isDisposed)
{
return;
}
if (disposing)
{
// Dispose managed resources.
if (this.CompressedStream != null)
{
this.CompressedStream.Dispose();
this.CompressedStream = null;
}
}
base.Dispose(disposing);
// Call the appropriate methods to clean up
// unmanaged resources here.
// Note disposing is done.
this.isDisposed = true;
}
[MemberNotNullWhen(true, nameof(CompressedStream))]
private bool InitializeInflateStream(bool isCriticalChunk)
{
// Read the zlib header : http://tools.ietf.org/html/rfc1950
// CMF(Compression Method and flags)
// This byte is divided into a 4 - bit compression method and a
// 4-bit information field depending on the compression method.
// bits 0 to 3 CM Compression method
// bits 4 to 7 CINFO Compression info
//
// 0 1
// +---+---+
// |CMF|FLG|
// +---+---+
int cmf = this.innerStream.ReadByte();
int flag = this.innerStream.ReadByte();
this.currentDataRemaining -= 2;
if (cmf == -1 || flag == -1)
{
return false;
}
if ((cmf & 0x0F) == 8)
{
// CINFO is the base-2 logarithm of the LZ77 window size, minus eight.
int cinfo = (cmf & 0xF0) >> 4;
if (cinfo > 7)
{
if (isCriticalChunk)
{
// Values of CINFO above 7 are not allowed in RFC1950.
// CINFO is not defined in this specification for CM not equal to 8.
throw new ImageFormatException($"Invalid window size for ZLIB header: cinfo={cinfo}");
}
return false;
}
}
else if (isCriticalChunk)
{
throw new ImageFormatException($"Bad method for ZLIB header: cmf={cmf}");
}
else
{
return false;
}
// The preset dictionary.
bool fdict = (flag & 32) != 0;
if (fdict)
{
// We don't need this for inflate so simply skip by the next four bytes.
// https://tools.ietf.org/html/rfc1950#page-6
if (this.innerStream.Read(ChecksumBuffer, 0, 4) != 4)
{
return false;
}
this.currentDataRemaining -= 4;
}
// Initialize the deflate BufferedReadStream.
this.CompressedStream = new DeflateStream(this, CompressionMode.Decompress, true);
return true;
}
}

19
src/ImageSharp/Configuration.cs

@ -2,9 +2,13 @@
// Licensed under the Six Labors Split License.
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Ani;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Exr;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Heif;
using SixLabors.ImageSharp.Formats.Ico;
@ -38,6 +42,9 @@ public sealed class Configuration
/// <summary>
/// Initializes a new instance of the <see cref="Configuration" /> class.
/// </summary>
// Every image operation requires a Configuration. Attaching the dependency to its constructors keeps the compile-only
// seed graph visible to modern .NET trimmers without executing that graph or adding module-initialization work.
[DynamicDependency(nameof(AotCompilerTools.SeedPixelOperations), typeof(AotCompilerTools))]
public Configuration()
{
}
@ -46,6 +53,8 @@ public sealed class Configuration
/// Initializes a new instance of the <see cref="Configuration" /> class.
/// </summary>
/// <param name="configurationModules">A collection of configuration modules to register.</param>
// The default Configuration is created through this overload, while callers may use either constructor.
[DynamicDependency(nameof(AotCompilerTools.SeedPixelOperations), typeof(AotCompilerTools))]
public Configuration(params IImageFormatConfigurationModule[] configurationModules)
{
if (configurationModules != null)
@ -65,7 +74,9 @@ public sealed class Configuration
/// <summary>
/// Gets or sets the maximum number of concurrent tasks enabled in ImageSharp algorithms
/// configured with this <see cref="Configuration"/> instance.
/// Initialized with <see cref="Environment.ProcessorCount"/> by default.
/// A positive value limits the number of concurrent operations to the set value.
/// If set to <c>-1</c>, there is no limit on the number of concurrently running operations.
/// Defaults to <see cref="Environment.ProcessorCount"/>.
/// </summary>
public int MaxDegreeOfParallelism
{
@ -213,8 +224,10 @@ public sealed class Configuration
/// <see cref="TgaConfigurationModule"/>.
/// <see cref="TiffConfigurationModule"/>.
/// <see cref="WebpConfigurationModule"/>.
/// <see cref="ExrConfigurationModule"/>.
/// <see cref="QoiConfigurationModule"/>.
/// <see cref="HeifConfigurationModule"/>.
/// <see cref="AniConfigurationModule"/>.
/// </summary>
/// <returns>The default configuration of <see cref="Configuration"/>.</returns>
internal static Configuration CreateDefaultInstance() => new(
@ -226,8 +239,10 @@ public sealed class Configuration
new TgaConfigurationModule(),
new TiffConfigurationModule(),
new WebpConfigurationModule(),
new ExrConfigurationModule(),
new QoiConfigurationModule(),
new HeifConfigurationModule(),
new IcoConfigurationModule(),
new CurConfigurationModule());
new CurConfigurationModule(),
new AniConfigurationModule());
}

73
src/ImageSharp/Formats/Ani/AniChunkType.cs

@ -0,0 +1,73 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Identifies top-level ANI RIFF chunks.
/// </summary>
internal enum AniChunkType : uint
{
/// <summary>
/// The animation header chunk, "anih".
/// </summary>
Header = 0x68_69_6E_61,
/// <summary>
/// The frame sequence chunk, "seq ".
/// </summary>
Sequence = 0x20_71_65_73,
/// <summary>
/// The per-step display-rate chunk, "rate".
/// </summary>
Rate = 0x65_74_61_72,
/// <summary>
/// A RIFF list chunk, "LIST".
/// </summary>
List = 0x54_53_49_4C
}
/// <summary>
/// Identifies ANI RIFF list types.
/// </summary>
internal enum AniListType : uint
{
/// <summary>
/// The information list, "INFO".
/// </summary>
Info = 0x4F_46_4E_49,
/// <summary>
/// The embedded frame-resource list, "fram".
/// </summary>
Frames = 0x6D_61_72_66
}
/// <summary>
/// Identifies chunks stored in an ANI information list.
/// </summary>
internal enum AniInfoChunkType : uint
{
/// <summary>
/// The animation name, "INAM".
/// </summary>
Name = 0x4D_41_4E_49,
/// <summary>
/// The animation artist, "IART".
/// </summary>
Artist = 0x54_52_41_49
}
/// <summary>
/// Identifies chunks stored in an ANI frame list.
/// </summary>
internal enum AniFrameChunkType : uint
{
/// <summary>
/// An embedded frame resource, "icon".
/// </summary>
Icon = 0x6E_6F_63_69
}

25
src/ImageSharp/Formats/Ani/AniConfigurationModule.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Registers the image encoder, decoder, and format detector for the ANI format.
/// </summary>
public sealed class AniConfigurationModule : IImageFormatConfigurationModule
{
/// <summary>
/// Initializes a new instance of the <see cref="AniConfigurationModule"/> class.
/// </summary>
public AniConfigurationModule()
{
}
/// <inheritdoc/>
public void Configure(Configuration configuration)
{
configuration.ImageFormatsManager.SetEncoder(AniFormat.Instance, new AniEncoder());
configuration.ImageFormatsManager.SetDecoder(AniFormat.Instance, AniDecoder.Instance);
configuration.ImageFormatsManager.AddImageFormatDetector(new AniImageFormatDetector());
}
}

54
src/ImageSharp/Formats/Ani/AniConstants.cs

@ -0,0 +1,54 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Defines constants used by the ANI format.
/// </summary>
internal static class AniConstants
{
/// <summary>
/// The number of bytes in the RIFF identifier, size, and form type.
/// </summary>
public const int RiffHeaderSize = 12;
/// <summary>
/// The number of bytes in a RIFF chunk identifier and size.
/// </summary>
public const int ChunkHeaderSize = 8;
/// <summary>
/// The number of bytes required to identify an embedded ICO or CUR resource.
/// </summary>
public const int IconDirHeaderSize = 6;
/// <summary>
/// The maximum number of bytes retained from an ancillary chunk.
/// </summary>
/// <remarks>
/// Control arrays and information strings come from untrusted input. Bounding them independently of the allocator
/// prevents a physically large RIFF chunk from consuming an unreasonable amount of memory.
/// </remarks>
public const int MaxAncillaryChunkSize = 8 * 1024 * 1024;
/// <summary>
/// The list of MIME types that identify ANI data.
/// </summary>
public static readonly IEnumerable<string> MimeTypes = ["application/x-navi-animation"];
/// <summary>
/// The list of file extensions that identify ANI data.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = ["ani"];
/// <summary>
/// Gets the RIFF container identifier.
/// </summary>
public static ReadOnlySpan<byte> RiffFourCc => "RIFF"u8;
/// <summary>
/// Gets the ANI RIFF form type.
/// </summary>
public static ReadOnlySpan<byte> AniFormTypeFourCc => "ACON"u8;
}

52
src/ImageSharp/Formats/Ani/AniDecoder.cs

@ -0,0 +1,52 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Decodes Windows animated cursor images.
/// </summary>
public sealed class AniDecoder : ImageDecoder
{
/// <summary>
/// Prevents a default instance of the <see cref="AniDecoder"/> class from being created.
/// </summary>
private AniDecoder()
{
}
/// <summary>
/// Gets the shared instance.
/// </summary>
public static AniDecoder Instance { get; } = new();
/// <inheritdoc/>
protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
{
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream));
using AniDecoderCore decoder = new(options);
Image<TPixel> image = decoder.Decode<TPixel>(options.Configuration, stream, cancellationToken);
ScaleToTargetSize(options, image);
return image;
}
/// <inheritdoc/>
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
=> this.Decode<Rgba32>(options, stream, cancellationToken);
/// <inheritdoc/>
protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
{
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream));
using AniDecoderCore decoder = new(options);
return decoder.Identify(options.Configuration, stream, cancellationToken);
}
}

862
src/ImageSharp/Formats/Ani/AniDecoderCore.cs

@ -0,0 +1,862 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Buffers.Binary;
using System.Runtime.InteropServices;
using System.Text;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Performs ANI decoding and identification.
/// </summary>
internal sealed class AniDecoderCore : ImageDecoderCore, IDisposable
{
private readonly List<(long Start, long End)> frameLists = new(1);
private readonly ImageMetadata imageMetadata;
private readonly AniMetadata aniMetadata;
private AniHeader header;
private IMemoryOwner<uint>? sequence;
private IMemoryOwner<uint>? rates;
/// <summary>
/// Reusable storage for the fixed ANI header and smaller RIFF values.
/// </summary>
private InlineArray36<byte> buffer;
/// <summary>
/// Initializes a new instance of the <see cref="AniDecoderCore"/> class.
/// </summary>
/// <param name="options">The general decoder options.</param>
public AniDecoderCore(DecoderOptions options)
: base(options)
{
// The decoded ANI metadata must belong to the same ImageMetadata instance transferred to Image or ImageInfo.
this.imageMetadata = new ImageMetadata();
this.aniMetadata = this.imageMetadata.GetAniMetadata();
}
/// <inheritdoc/>
protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.ParseContainer(stream);
DecoderOptions frameOptions = this.CreateFrameDecoderOptions();
List<(AniFrameFormat Format, Image<TPixel> Image)?> resources = [];
List<ImageFrame<TPixel>> outputFrames = [];
// Until Image accepts the frame collection, this method remains responsible for disposing every constructed output frame.
bool outputFramesOwned = false;
try
{
// Container parsing runs first because seq/rate chunks can occur after the frame list and affect how resources are projected.
resources.EnsureCapacity((int)Math.Min(this.header.FrameCount, this.Options.MaxFrames));
this.ProcessFrameChunks(stream, resources, (format, frameStream) =>
{
cancellationToken.ThrowIfCancellationRequested();
Image<TPixel> resource = DecodeFrame<TPixel>(format, frameOptions, frameStream, cancellationToken);
this.Dimensions = new Size(Math.Max(this.Dimensions.Width, resource.Width), Math.Max(this.Dimensions.Height, resource.Height));
return resource;
});
if (resources.Count is 0)
{
throw new InvalidImageContentException("The ANI file does not contain any frame resources.");
}
// Keep the owners alive and resolve their spans once; sequence and rate lookup occurs for every animation step.
IMemoryOwner<uint>? sequenceOwner = this.sequence;
bool hasSequence = sequenceOwner is not null;
ReadOnlySpan<uint> sequence = sequenceOwner is null ? [] : sequenceOwner.GetSpan();
ReadOnlySpan<uint> rates = this.rates is null ? [] : this.rates.GetSpan();
int stepCount = hasSequence ? sequence.Length : resources.Count;
int maxFrames = (int)this.Options.MaxFrames;
outputFrames.EnsureCapacity(Math.Min(maxFrames, resources.Count));
for (int step = 0; step < stepCount && outputFrames.Count < maxFrames; step++)
{
cancellationToken.ThrowIfCancellationRequested();
uint resourceIndex = hasSequence ? sequence[step] : (uint)step;
if (resourceIndex >= resources.Count || resources[(int)resourceIndex] is not { } resource)
{
// A bad ordering entry is recoverable ancillary data: the remaining valid steps can still be decoded.
this.ExecuteAncillarySegmentAction(() => throw new InvalidImageContentException("The ANI sequence references a missing frame resource."));
continue;
}
(AniFrameFormat format, Image<TPixel> resourceImage) = resource;
uint frameDelay = step < rates.Length ? rates[step] : this.aniMetadata.DisplayRate;
for (int i = 0; i < resourceImage.Frames.Count && outputFrames.Count < maxFrames; i++)
{
ImageFrame<TPixel> source = resourceImage.Frames[i];
ImageFrame<TPixel> target = new(this.Options.Configuration, this.Dimensions);
// ANI flattens differently sized ICO/CUR variants into one ImageSharp frame collection.
// The common canvas preserves that invariant, while encoding dimensions retain the source size.
for (int y = 0; y < source.Height; y++)
{
source.PixelBuffer.DangerousGetRowSpan(y).CopyTo(target.PixelBuffer.DangerousGetRowSpan(y));
}
AniFrameMetadata metadata = CreateFrameMetadata(source.Metadata, format, step + 1, frameDelay, source.Size);
target.Metadata.SetFormatMetadata(AniFormat.Instance, metadata);
outputFrames.Add(target);
}
}
if (outputFrames.Count is 0)
{
throw new InvalidImageContentException("The ANI file does not contain any decodable animation steps.");
}
// Image takes ownership of the supplied frames; only the temporary decoded resources remain locally owned.
Image<TPixel> image = new(this.Options.Configuration, this.imageMetadata, outputFrames);
outputFramesOwned = true;
return image;
}
finally
{
// Embedded images are temporary resource containers; their pixels have already been copied to the flattened output frames.
foreach ((AniFrameFormat Format, Image<TPixel> Image)? resource in resources)
{
if (resource is { } value)
{
value.Image.Dispose();
}
}
// Construction failures occur before Image can own the frames, so the partial collection must be released here.
if (!outputFramesOwned)
{
foreach (ImageFrame<TPixel> frame in outputFrames)
{
frame.Dispose();
}
}
}
}
/// <inheritdoc/>
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.ParseContainer(stream);
DecoderOptions frameOptions = this.CreateFrameDecoderOptions();
List<(AniFrameFormat Format, ImageInfo Info)?> resources = [];
resources.EnsureCapacity((int)Math.Min(this.header.FrameCount, this.Options.MaxFrames));
this.ProcessFrameChunks(stream, resources, (format, frameStream) =>
{
cancellationToken.ThrowIfCancellationRequested();
ImageInfo info = IdentifyFrame(format, frameOptions, frameStream, cancellationToken);
this.Dimensions = new Size(Math.Max(this.Dimensions.Width, info.Width), Math.Max(this.Dimensions.Height, info.Height));
return info;
});
if (resources.Count is 0)
{
throw new InvalidImageContentException("The ANI file does not contain any frame resources.");
}
// Identification mirrors decode without allocating pixels, while preserving the same step-to-resource projection.
List<ImageFrameMetadata> outputFrames = [];
IMemoryOwner<uint>? sequenceOwner = this.sequence;
bool hasSequence = sequenceOwner is not null;
ReadOnlySpan<uint> sequence = sequenceOwner is null ? [] : sequenceOwner.GetSpan();
ReadOnlySpan<uint> rates = this.rates is null ? [] : this.rates.GetSpan();
int stepCount = hasSequence ? sequence.Length : resources.Count;
int maxFrames = (int)this.Options.MaxFrames;
_ = outputFrames.EnsureCapacity(Math.Min(maxFrames, resources.Count));
for (int step = 0; step < stepCount && outputFrames.Count < maxFrames; step++)
{
cancellationToken.ThrowIfCancellationRequested();
uint resourceIndex = hasSequence ? sequence[step] : (uint)step;
if (resourceIndex >= resources.Count || resources[(int)resourceIndex] is not { } resource)
{
// Sequence errors are ancillary during identification for the same reason as decoding: other steps remain usable.
this.ExecuteAncillarySegmentAction(() => throw new InvalidImageContentException("The ANI sequence references a missing frame resource."));
continue;
}
(AniFrameFormat format, ImageInfo info) = resource;
uint frameDelay = step < rates.Length ? rates[step] : this.aniMetadata.DisplayRate;
if (info.FrameMetadataCollection.Count is 0)
{
// Some embedded decoders expose only resource-level dimensions, so synthesize the one required ANI frame entry.
ImageFrameMetadata target = new();
target.SetFormatMetadata(AniFormat.Instance, CreateFrameMetadata(null, format, step + 1, frameDelay, info.Size));
outputFrames.Add(target);
continue;
}
for (int i = 0; i < info.FrameMetadataCollection.Count && outputFrames.Count < maxFrames; i++)
{
ImageFrameMetadata source = info.FrameMetadataCollection[i];
ImageFrameMetadata target = new();
target.SetFormatMetadata(AniFormat.Instance, CreateFrameMetadata(source, format, step + 1, frameDelay, info.Size));
outputFrames.Add(target);
}
}
if (outputFrames.Count is 0)
{
throw new InvalidImageContentException("The ANI file does not contain any identifiable animation steps.");
}
return new ImageInfo(this.Dimensions, this.imageMetadata, outputFrames);
}
/// <summary>
/// Parses the RIFF container and records frame-list boundaries for subsequent embedded decoding.
/// </summary>
/// <param name="stream">The ANI stream.</param>
private void ParseContainer(BufferedReadStream stream)
{
// Parser-owned chunk state is replaced by the container currently being scanned.
this.frameLists.Clear();
this.sequence?.Dispose();
this.rates?.Dispose();
this.sequence = null;
this.rates = null;
long containerStart = stream.Position;
Span<byte> riffHeader = this.buffer[..AniConstants.RiffHeaderSize];
ReadExactly(stream, riffHeader, "RIFF header");
if (!riffHeader[..4].SequenceEqual(AniConstants.RiffFourCc)
|| !riffHeader.Slice(8, 4).SequenceEqual(AniConstants.AniFormTypeFourCc))
{
throw new InvalidImageContentException("The stream does not contain an ANI RIFF container.");
}
uint declaredSize = BinaryPrimitives.ReadUInt32LittleEndian(riffHeader[4..]);
if (declaredSize < sizeof(uint))
{
throw new InvalidImageContentException("The ANI RIFF container size is invalid.");
}
// RIFF size excludes the initial identifier and size field. Some real-world ANI files incorrectly
// include those eight bytes, so the physical stream length remains the hard read boundary.
long declaredEnd = checked(containerStart + 8 + declaredSize);
long containerEnd = Math.Min(declaredEnd, stream.Length);
bool headerFound = false;
while (stream.Position + AniConstants.ChunkHeaderSize <= containerEnd)
{
AniRiffChunkHeader chunk = this.ReadChunkHeader(stream);
long dataEnd = GetChunkDataEnd(stream, chunk.Size, containerEnd);
switch ((AniChunkType)chunk.FourCc)
{
case AniChunkType.Header:
this.ReadAniHeader(stream, chunk.Size);
headerFound = true;
break;
case AniChunkType.Sequence:
// Ordering and timing affect presentation, not pixel decoding, so malformed chunks follow ancillary handling.
this.ExecuteAncillarySegmentAction(() => this.ReadUInt32Values(stream, chunk.Size, "sequence", ref this.sequence));
break;
case AniChunkType.Rate:
this.ExecuteAncillarySegmentAction(() => this.ReadUInt32Values(stream, chunk.Size, "rate", ref this.rates));
break;
case AniChunkType.List:
this.ReadList(stream, dataEnd);
break;
}
stream.Position = GetPaddedEnd(dataEnd, chunk.Size, containerEnd);
}
if (!headerFound)
{
throw new InvalidImageContentException("The ANI file does not contain an animation header.");
}
}
/// <summary>
/// Parses the mandatory 36-byte ANI header and copies its observable values to image metadata.
/// </summary>
/// <param name="stream">The ANI stream.</param>
/// <param name="chunkSize">The ANI header chunk size.</param>
private void ReadAniHeader(BufferedReadStream stream, uint chunkSize)
{
if (chunkSize < AniHeader.Size)
{
throw new InvalidImageContentException("The ANI animation header is truncated.");
}
Span<byte> data = this.buffer;
ReadExactly(stream, data, "ANI header");
this.header = AniHeader.Parse(data);
if (this.header.BytesInHeader < AniHeader.Size || this.header.BytesInHeader > chunkSize)
{
throw new InvalidImageContentException("The ANI animation header declares an invalid size.");
}
this.aniMetadata.Width = this.header.Width;
this.aniMetadata.Height = this.header.Height;
this.aniMetadata.BitCount = this.header.BitCount;
this.aniMetadata.Planes = this.header.Planes;
this.aniMetadata.DisplayRate = this.header.DisplayRate;
this.aniMetadata.Flags = this.header.Flags;
}
/// <summary>
/// Reads a RIFF list type and records or parses its contents.
/// </summary>
/// <param name="stream">The ANI stream.</param>
/// <param name="listEnd">The exclusive end of the list payload.</param>
private void ReadList(BufferedReadStream stream, long listEnd)
{
if (listEnd - stream.Position < sizeof(uint))
{
throw new InvalidImageContentException("The ANI file contains a truncated RIFF list.");
}
Span<byte> typeData = this.buffer[..sizeof(uint)];
ReadExactly(stream, typeData, "RIFF list type");
AniListType type = (AniListType)BinaryPrimitives.ReadUInt32LittleEndian(typeData);
switch (type)
{
case AniListType.Frames:
// Defer nested decoding until the complete container has supplied any later seq/rate chunks.
this.frameLists.Add((stream.Position, listEnd));
break;
case AniListType.Info when !this.Options.SkipMetadata:
this.ExecuteAncillarySegmentAction(() => this.ReadInfoList(stream, listEnd));
break;
}
}
/// <summary>
/// Parses the optional ANI name and artist information.
/// </summary>
/// <param name="stream">The ANI stream.</param>
/// <param name="listEnd">The exclusive end of the information list.</param>
private void ReadInfoList(BufferedReadStream stream, long listEnd)
{
// INAM and IART are consumed sequentially, so one grow-only buffer covers every text chunk in the list.
IMemoryOwner<byte>? textOwner = null;
try
{
while (stream.Position + AniConstants.ChunkHeaderSize <= listEnd)
{
AniRiffChunkHeader chunk = this.ReadChunkHeader(stream);
long dataEnd = GetChunkDataEnd(stream, chunk.Size, listEnd);
switch ((AniInfoChunkType)chunk.FourCc)
{
case AniInfoChunkType.Name:
if (this.TryReadText(stream, chunk.Size, ref textOwner, out string? name))
{
this.aniMetadata.Name = name;
}
break;
case AniInfoChunkType.Artist:
if (this.TryReadText(stream, chunk.Size, ref textOwner, out string? artist))
{
this.aniMetadata.Artist = artist;
}
break;
}
stream.Position = GetPaddedEnd(dataEnd, chunk.Size, listEnd);
}
}
finally
{
textOwner?.Dispose();
}
}
/// <summary>
/// Reads a sequence or rate chunk into reusable allocator-owned memory.
/// </summary>
/// <param name="stream">The ANI stream.</param>
/// <param name="chunkSize">The chunk payload size.</param>
/// <param name="description">The chunk description used in error messages.</param>
/// <param name="owner">The buffer to reuse or replace.</param>
private void ReadUInt32Values(BufferedReadStream stream, uint chunkSize, string description, ref IMemoryOwner<uint>? owner)
{
// seq and rate payloads are DWORD arrays; trailing bytes cannot form a valid entry.
if (chunkSize % sizeof(uint) is not 0)
{
this.ThrowOrIgnoreNonStrictSegmentError($"The ANI {description} chunk has an invalid size.");
return;
}
// MaxFrames controls retained animation steps, but its default is intentionally unbounded. Apply a separate
// byte limit before allocation so an oversized control chunk follows ancillary integrity handling.
if (chunkSize > AniConstants.MaxAncillaryChunkSize)
{
this.ThrowOrIgnoreNonStrictSegmentError($"The ANI {description} chunk is too large.");
return;
}
int count = (int)Math.Min(chunkSize / sizeof(uint), this.Options.MaxFrames);
if (count is 0)
{
this.ThrowOrIgnoreNonStrictSegmentError($"The ANI {description} chunk does not contain any values.");
return;
}
IMemoryOwner<uint> valuesOwner;
bool replaceOwner;
// Duplicate chunks can overwrite an equal-sized allocation. A different size uses a replacement so a failed read
// leaves the last valid chunk available to non-strict decoding.
if (owner is not null && owner.GetSpan().Length == count)
{
valuesOwner = owner;
replaceOwner = false;
}
else
{
valuesOwner = this.Options.Configuration.MemoryAllocator.Allocate<uint>(count);
replaceOwner = true;
}
bool success = false;
// A newly allocated replacement is not published until the entire payload has been read and normalized.
try
{
Span<uint> values = valuesOwner.GetSpan()[..count];
Span<byte> data = MemoryMarshal.AsBytes(values);
if (stream.Read(data) != data.Length)
{
this.ThrowOrIgnoreNonStrictSegmentError($"Not enough bytes to read the ANI {description} chunk.");
return;
}
if (!BitConverter.IsLittleEndian)
{
// RIFF integers are always little-endian; normalize once here so hot step loops use native uint indexing.
for (int i = 0; i < values.Length; i++)
{
values[i] = BinaryPrimitives.ReverseEndianness(values[i]);
}
}
success = true;
}
finally
{
if (!success && replaceOwner)
{
valuesOwner.Dispose();
}
}
if (replaceOwner)
{
owner?.Dispose();
owner = valuesOwner;
}
}
/// <inheritdoc/>
public void Dispose()
{
this.sequence?.Dispose();
this.rates?.Dispose();
this.sequence = null;
this.rates = null;
}
/// <summary>
/// Tries to read a null-terminated ANI information string.
/// </summary>
/// <param name="stream">The ANI stream.</param>
/// <param name="chunkSize">The text chunk payload size.</param>
/// <param name="owner">The reusable text buffer.</param>
/// <param name="value">The decoded ASCII text when successful.</param>
/// <returns><see langword="true"/> when the text was read successfully; otherwise, <see langword="false"/>.</returns>
private bool TryReadText(BufferedReadStream stream, uint chunkSize, ref IMemoryOwner<byte>? owner, out string? value)
{
value = null;
// INFO text is optional metadata. Reject or skip oversized values before renting their backing buffer.
if (chunkSize > AniConstants.MaxAncillaryChunkSize)
{
this.ThrowOrIgnoreNonStrictSegmentError("The ANI information text chunk is too large.");
return false;
}
int length = (int)chunkSize;
// Retain the largest text buffer encountered because INFO values are decoded one at a time.
if (owner is null || owner.GetSpan().Length < length)
{
owner?.Dispose();
owner = this.Options.Configuration.MemoryAllocator.Allocate<byte>(length);
}
Span<byte> data = owner.GetSpan()[..length];
if (stream.Read(data) != data.Length)
{
this.ThrowOrIgnoreNonStrictSegmentError("Not enough bytes to read the ANI information text.");
return false;
}
// RIFF text is null-terminated, but the declared chunk may include bytes after the first terminator.
int terminator = data.IndexOf((byte)0);
value = Encoding.ASCII.GetString(terminator < 0 ? data : data[..terminator]);
return true;
}
/// <summary>
/// Processes each embedded frame-resource chunk without allowing its decoder to read adjacent RIFF data.
/// </summary>
/// <typeparam name="T">The parsed resource type.</typeparam>
/// <param name="stream">The ANI stream.</param>
/// <param name="resources">The destination resource slots.</param>
/// <param name="action">The operation to perform for each resource format and bounded stream.</param>
private void ProcessFrameChunks<T>(BufferedReadStream stream, List<(AniFrameFormat Format, T Resource)?> resources, Func<AniFrameFormat, Stream, T> action)
where T : class
{
// Child decoding is synchronous, so one bounded stream object can be repositioned for every physical resource.
AniFrameStream frameStream = new(stream);
ReadOnlySpan<uint> sequence = this.sequence is null ? [] : this.sequence.GetSpan();
bool hasSequence = this.sequence is not null;
int decodedResourceCount = 0;
int maxDecodedResources = (int)this.Options.MaxFrames;
IMemoryOwner<uint>? sortedSequenceOwner = null;
try
{
ReadOnlySpan<uint> requiredResources = sequence;
if (hasSequence)
{
bool isSorted = true;
for (int i = 1; i < sequence.Length; i++)
{
if (sequence[i] < sequence[i - 1])
{
isSorted = false;
break;
}
}
if (!isSorted)
{
// Playback order can reference resources arbitrarily. A sorted allocator-owned copy turns the physical
// resource scan into a linear merge instead of searching the complete sequence for every icon chunk.
sortedSequenceOwner = this.Options.Configuration.MemoryAllocator.Allocate<uint>(sequence.Length);
Span<uint> sortedSequence = sortedSequenceOwner.GetSpan();
sequence.CopyTo(sortedSequence);
sortedSequence.Sort();
requiredResources = sortedSequence;
}
}
int requiredResourceIndex = 0;
uint lastRequiredResource = hasSequence ? requiredResources[^1] : 0;
foreach ((long start, long end) in this.frameLists)
{
stream.Position = start;
while (stream.Position + AniConstants.ChunkHeaderSize <= end)
{
AniRiffChunkHeader chunk = this.ReadChunkHeader(stream);
long dataStart = stream.Position;
long dataEnd = GetChunkDataEnd(stream, chunk.Size, end);
if ((AniFrameChunkType)chunk.FourCc is AniFrameChunkType.Icon)
{
int resourceIndex = resources.Count;
// Sequence entries index the physical resource table, so ignored corrupt resources retain an empty slot.
resources.Add(null);
if (hasSequence)
{
while (requiredResourceIndex < requiredResources.Length && requiredResources[requiredResourceIndex] < (uint)resourceIndex)
{
requiredResourceIndex++;
}
}
// Unsequenced resources are consumed in physical order; sequenced files need only the referenced indices.
bool shouldDecode = !hasSequence
|| (requiredResourceIndex < requiredResources.Length && requiredResources[requiredResourceIndex] == (uint)resourceIndex);
if (shouldDecode)
{
this.ExecuteImageDataSegmentAction(() =>
{
// Child decoders may seek according to embedded offsets; the bounded view prevents crossing the icon chunk.
frameStream.Reset(dataStart, chunk.Size);
AniFrameFormat format = this.GetFrameFormat(frameStream);
// Format probing consumes the directory prefix, while the selected child decoder requires the complete resource.
frameStream.Position = 0;
resources[resourceIndex] = (format, action(format, frameStream));
});
if (resources[resourceIndex] is not null)
{
decodedResourceCount++;
}
}
// Every decoded resource contributes at least one output frame, while a sequence cannot reference later indices.
if ((!hasSequence && decodedResourceCount == maxDecodedResources)
|| (hasSequence && (uint)resourceIndex == lastRequiredResource))
{
return;
}
}
stream.Position = GetPaddedEnd(dataEnd, chunk.Size, end);
}
}
}
finally
{
sortedSequenceOwner?.Dispose();
}
}
/// <summary>
/// Determines the embedded resource format from the ANI header and ICO/CUR directory prefix.
/// </summary>
/// <param name="stream">The bounded frame-resource stream.</param>
/// <returns>The embedded resource format.</returns>
private AniFrameFormat GetFrameFormat(Stream stream)
{
// Without AF_ICON, the icon chunk payload is a raw DIB and has no ICO/CUR directory prefix to inspect.
if (!this.header.Flags.HasFlag(AniHeaderFlags.IsIcon))
{
return AniFrameFormat.Bmp;
}
Span<byte> iconHeader = this.buffer[..AniConstants.IconDirHeaderSize];
if (stream.Read(iconHeader) != iconHeader.Length)
{
throw new InvalidImageContentException("The ANI file contains a truncated ICO or CUR resource.");
}
IconFileType type = (IconFileType)BinaryPrimitives.ReadUInt16LittleEndian(iconHeader[2..]);
return type switch
{
IconFileType.ICO => AniFrameFormat.Ico,
IconFileType.CUR => AniFrameFormat.Cur,
_ => throw new InvalidImageContentException("The ANI file contains an unsupported icon resource.")
};
}
/// <summary>
/// Decodes one embedded ANI frame resource.
/// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
/// <param name="format">The embedded resource format.</param>
/// <param name="options">The nested decoder options.</param>
/// <param name="stream">The bounded resource stream.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>The decoded resource.</returns>
private static Image<TPixel> DecodeFrame<TPixel>(AniFrameFormat format, DecoderOptions options, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
=> format switch
{
AniFrameFormat.Ico => new IcoDecoderCore(options).Decode<TPixel>(options.Configuration, stream, cancellationToken),
AniFrameFormat.Cur => new CurDecoderCore(options).Decode<TPixel>(options.Configuration, stream, cancellationToken),
AniFrameFormat.Bmp => new BmpDecoderCore(new BmpDecoderOptions
{
GeneralOptions = options,
SkipFileHeader = true
}).Decode<TPixel>(options.Configuration, stream, cancellationToken),
_ => throw new InvalidImageContentException("The ANI file contains an unsupported frame format.")
};
/// <summary>
/// Identifies one embedded ANI frame resource.
/// </summary>
/// <param name="format">The embedded resource format.</param>
/// <param name="options">The nested decoder options.</param>
/// <param name="stream">The bounded resource stream.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>The identified resource.</returns>
private static ImageInfo IdentifyFrame(AniFrameFormat format, DecoderOptions options, Stream stream, CancellationToken cancellationToken)
=> format switch
{
AniFrameFormat.Ico => new IcoDecoderCore(options).Identify(options.Configuration, stream, cancellationToken),
AniFrameFormat.Cur => new CurDecoderCore(options).Identify(options.Configuration, stream, cancellationToken),
AniFrameFormat.Bmp => new BmpDecoderCore(new BmpDecoderOptions
{
GeneralOptions = options,
SkipFileHeader = true
}).Identify(options.Configuration, stream, cancellationToken),
_ => throw new InvalidImageContentException("The ANI file contains an unsupported frame format.")
};
/// <summary>
/// Creates ANI metadata for one flattened output frame.
/// </summary>
/// <param name="source">The embedded frame metadata, when available.</param>
/// <param name="format">The embedded resource format.</param>
/// <param name="sequenceNumber">The animation sequence number.</param>
/// <param name="frameDelay">The display rate in sixtieths of a second.</param>
/// <param name="size">The embedded frame size.</param>
/// <returns>The ANI frame metadata.</returns>
private static AniFrameMetadata CreateFrameMetadata(ImageFrameMetadata? source, AniFrameFormat format, int sequenceNumber, uint frameDelay, Size size)
{
AniFrameMetadata metadata = new()
{
FrameDelay = frameDelay,
SequenceNumber = sequenceNumber,
FrameFormat = format
};
if (source is null)
{
metadata.EncodingWidth = NarrowDimension(size.Width);
metadata.EncodingHeight = NarrowDimension(size.Height);
return metadata;
}
// ColorTable is managed read-only memory and remains valid after the temporary child image is disposed,
// so the flattened metadata can retain the same view without cloning its backing array.
switch (format)
{
case AniFrameFormat.Ico:
IcoFrameMetadata icoMetadata = source.GetIcoMetadata();
metadata.EncodingWidth = icoMetadata.EncodingWidth;
metadata.EncodingHeight = icoMetadata.EncodingHeight;
metadata.Compression = icoMetadata.Compression;
metadata.BmpBitsPerPixel = icoMetadata.BmpBitsPerPixel;
metadata.ColorTable = icoMetadata.ColorTable;
break;
case AniFrameFormat.Cur:
CurFrameMetadata curMetadata = source.GetCurMetadata();
metadata.EncodingWidth = curMetadata.EncodingWidth;
metadata.EncodingHeight = curMetadata.EncodingHeight;
metadata.Compression = curMetadata.Compression;
metadata.BmpBitsPerPixel = curMetadata.BmpBitsPerPixel;
metadata.HotspotX = curMetadata.HotspotX;
metadata.HotspotY = curMetadata.HotspotY;
metadata.ColorTable = curMetadata.ColorTable;
break;
case AniFrameFormat.Bmp:
metadata.EncodingWidth = NarrowDimension(size.Width);
metadata.EncodingHeight = NarrowDimension(size.Height);
break;
}
return metadata;
}
/// <summary>
/// Creates decoder options for embedded resources without applying the outer ANI resize twice.
/// </summary>
/// <returns>The embedded frame decoder options.</returns>
private DecoderOptions CreateFrameDecoderOptions()
=> new()
{
Configuration = this.Options.Configuration,
MaxFrames = this.Options.MaxFrames,
SkipMetadata = this.Options.SkipMetadata,
SegmentIntegrityHandling = this.Options.SegmentIntegrityHandling,
ColorProfileHandling = this.Options.ColorProfileHandling
};
/// <summary>
/// Reads one fixed-size RIFF chunk header.
/// </summary>
/// <param name="stream">The ANI stream.</param>
/// <returns>The parsed chunk header.</returns>
private AniRiffChunkHeader ReadChunkHeader(BufferedReadStream stream)
{
Span<byte> data = this.buffer[..AniConstants.ChunkHeaderSize];
ReadExactly(stream, data, "RIFF chunk header");
return AniRiffChunkHeader.Parse(data);
}
/// <summary>
/// Calculates and validates the exclusive end of a RIFF chunk payload.
/// </summary>
/// <param name="stream">The ANI stream.</param>
/// <param name="size">The declared payload size.</param>
/// <param name="containerEnd">The exclusive parent-container boundary.</param>
/// <returns>The exclusive payload boundary.</returns>
private static long GetChunkDataEnd(BufferedReadStream stream, uint size, long containerEnd)
{
long end = checked(stream.Position + size);
if (end > containerEnd)
{
throw new InvalidImageContentException("An ANI RIFF chunk extends beyond its containing list.");
}
return end;
}
/// <summary>
/// Calculates and validates the word-aligned end of a RIFF chunk.
/// </summary>
/// <param name="dataEnd">The exclusive payload boundary.</param>
/// <param name="size">The declared payload size.</param>
/// <param name="containerEnd">The exclusive parent-container boundary.</param>
/// <returns>The exclusive padded chunk boundary.</returns>
private static long GetPaddedEnd(long dataEnd, uint size, long containerEnd)
{
// RIFF aligns each chunk to a 16-bit boundary without including the optional pad byte in the declared size.
long paddedEnd = dataEnd + (size & 1);
if (paddedEnd > containerEnd)
{
throw new InvalidImageContentException("An ANI RIFF chunk is missing its alignment padding.");
}
return paddedEnd;
}
/// <summary>
/// Reads an exact number of bytes or reports a truncated ANI file.
/// </summary>
/// <param name="stream">The ANI stream.</param>
/// <param name="destination">The destination buffer.</param>
/// <param name="description">The data description used in the error message.</param>
private static void ReadExactly(BufferedReadStream stream, Span<byte> destination, string description)
{
if (stream.Read(destination) != destination.Length)
{
throw new InvalidImageContentException($"Not enough bytes to read the {description}.");
}
}
/// <summary>
/// Converts a pixel dimension to the one-byte ICO/CUR representation.
/// </summary>
/// <param name="value">The pixel dimension.</param>
/// <returns>The encoded dimension, where zero represents 256 pixels or greater.</returns>
private static byte NarrowDimension(int value) => value > byte.MaxValue ? (byte)0 : (byte)value;
}

24
src/ImageSharp/Formats/Ani/AniEncoder.cs

@ -0,0 +1,24 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Encodes images as Windows animated cursors.
/// </summary>
public sealed class AniEncoder : QuantizingImageEncoder
{
/// <summary>
/// Initializes a new instance of the <see cref="AniEncoder"/> class.
/// </summary>
public AniEncoder()
{
}
/// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
AniEncoderCore encoder = new(this);
encoder.Encode(image, stream, cancellationToken);
}
}

514
src/ImageSharp/Formats/Ani/AniEncoderCore.cs

@ -0,0 +1,514 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Buffers.Binary;
using System.Text;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Performs ANI encoding.
/// </summary>
internal sealed class AniEncoderCore
{
private readonly AniEncoder encoder;
// Each nested encoder is configured once and reused for every resource of that type in this ANI operation.
private IcoEncoderCore? icoEncoder;
private CurEncoderCore? curEncoder;
private BmpEncoderCore? bmpEncoder;
/// <summary>
/// Reusable storage for the fixed ANI header and smaller RIFF values.
/// </summary>
private InlineArray36<byte> buffer;
/// <summary>
/// Initializes a new instance of the <see cref="AniEncoderCore"/> class.
/// </summary>
/// <param name="encoder">The encoder options.</param>
public AniEncoderCore(AniEncoder encoder)
=> this.encoder = encoder;
/// <summary>
/// Encodes an image as ANI data.
/// </summary>
/// <typeparam name="TPixel">The source pixel type.</typeparam>
/// <param name="image">The source image.</param>
/// <param name="stream">The destination stream.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
AniMetadata imageMetadata = image.Metadata.GetAniMetadata();
AniFrameMetadata firstMetadata = image.Frames.RootFrame.Metadata.GetAniMetadata();
AniFrameFormat firstFormat = firstMetadata.FrameFormat;
bool bitmapResources = firstFormat is AniFrameFormat.Bmp;
bool writeSequence = imageMetadata.Flags.HasFlag(AniHeaderFlags.ContainsSequence);
uint displayRate = firstMetadata.FrameDelay is 0 ? imageMetadata.DisplayRate : firstMetadata.FrameDelay;
bool hasVariableRates = false;
int groupCount = 0;
int maxGroupSize = 1;
if (bitmapResources && imageMetadata.BitCount is not (0 or 1 or 2 or 4 or 8 or 16 or 24 or 32))
{
throw new ImageFormatException("ANI bitmap resources require a supported bit depth.");
}
if (bitmapResources && imageMetadata.Planes is not (0 or 1))
{
throw new ImageFormatException("ANI bitmap resources require exactly one color plane.");
}
// This validation pass derives the fixed ANI header and largest icon directory without allocating a grouping graph.
// Encoding repeats the linear grouping scan below, trading a cheap pass for zero per-group collections.
for (int frameIndex = 0; frameIndex < image.Frames.Count;)
{
AniFrameMetadata metadata = image.Frames[frameIndex].Metadata.GetAniMetadata();
int groupSize = 1;
if (metadata.FrameFormat is not (AniFrameFormat.Ico or AniFrameFormat.Cur or AniFrameFormat.Bmp))
{
// FrameFormat is public metadata and therefore must be validated before any container bytes are written.
throw new ImageFormatException("ANI contains an unsupported embedded frame format.");
}
// Positive sequence numbers group adjacent resolution variants; non-positive values form independent steps.
if (metadata.SequenceNumber > 0)
{
while (frameIndex + groupSize < image.Frames.Count && image.Frames[frameIndex + groupSize].Metadata.GetAniMetadata().SequenceNumber == metadata.SequenceNumber)
{
groupSize++;
}
}
if (bitmapResources != (metadata.FrameFormat is AniFrameFormat.Bmp))
{
// AF_ICON applies to the complete file, so raw DIB resources cannot coexist with ICO/CUR resources.
throw new ImageFormatException("ANI cannot mix bitmap resources with ICO or CUR resources.");
}
if (bitmapResources && groupSize > 1)
{
// Only ICO/CUR directories can contain multiple resolution variants in one physical resource.
throw new ImageFormatException("ANI bitmap resources cannot contain resolution variants.");
}
// All variants share one animation step, which requires one child format and one rate value.
for (int i = 1; i < groupSize; i++)
{
AniFrameMetadata current = image.Frames[frameIndex + i].Metadata.GetAniMetadata();
if (current.FrameFormat != metadata.FrameFormat)
{
throw new ImageFormatException("ANI resolution variants must use the same embedded format.");
}
if (current.FrameDelay != metadata.FrameDelay)
{
throw new ImageFormatException("ANI resolution variants must use the same frame delay.");
}
}
uint frameDelay = metadata.FrameDelay is 0 ? displayRate : metadata.FrameDelay;
hasVariableRates |= frameDelay != displayRate;
maxGroupSize = Math.Max(maxGroupSize, groupSize);
groupCount++;
frameIndex += groupSize;
}
// Icon-based ANI files leave global geometry and pixel layout at zero because each ICO/CUR entry owns those values.
AniHeader header = new()
{
BytesInHeader = AniHeader.Size,
FrameCount = (uint)groupCount,
StepCount = (uint)groupCount,
Width = bitmapResources ? imageMetadata.Width is 0 ? (uint)image.Width : imageMetadata.Width : 0,
Height = bitmapResources ? imageMetadata.Height is 0 ? (uint)image.Height : imageMetadata.Height : 0,
BitCount = bitmapResources ? imageMetadata.BitCount is 0 ? 32U : imageMetadata.BitCount : 0,
Planes = bitmapResources ? 1U : 0,
DisplayRate = displayRate,
Flags = (bitmapResources ? 0 : AniHeaderFlags.IsIcon) | (writeSequence ? AniHeaderFlags.ContainsSequence : 0)
};
// One allocator-owned directory buffer is sliced and reused for every icon resource; its capacity is the largest group.
using IMemoryOwner<IconEncoderCore.EncodingFrameMetadata>? iconEntriesOwner = bitmapResources ? null : image.Configuration.MemoryAllocator.Allocate<IconEncoderCore.EncodingFrameMetadata>(maxGroupSize);
Span<IconEncoderCore.EncodingFrameMetadata> iconEntries = iconEntriesOwner is null ? [] : iconEntriesOwner.GetSpan();
// ImageEncoder guarantees a seekable destination, allowing direct nested encoding and RIFF size backpatching.
long riffSizePosition = this.BeginContainer(stream, AniConstants.RiffFourCc, AniConstants.AniFormTypeFourCc);
this.WriteHeader(stream, header);
if (writeSequence)
{
this.WriteSequence(stream, groupCount);
}
if (hasVariableRates)
{
this.WriteRates(stream, image, displayRate);
}
if (!this.encoder.SkipMetadata && (imageMetadata.Name is not null || imageMetadata.Artist is not null))
{
this.WriteInfoList(stream, imageMetadata, image.Configuration.MemoryAllocator);
}
long frameListSizePosition = this.BeginContainer(stream, "LIST"u8, "fram"u8);
// Repeat the allocation-free adjacent grouping scan used by the validation pass.
for (int frameIndex = 0; frameIndex < image.Frames.Count;)
{
cancellationToken.ThrowIfCancellationRequested();
AniFrameMetadata metadata = image.Frames[frameIndex].Metadata.GetAniMetadata();
int groupSize = 1;
if (metadata.SequenceNumber > 0)
{
while (frameIndex + groupSize < image.Frames.Count && image.Frames[frameIndex + groupSize].Metadata.GetAniMetadata().SequenceNumber == metadata.SequenceNumber)
{
groupSize++;
}
}
long frameSizePosition = this.BeginChunk(stream, "icon"u8);
this.WriteFrameResource(image, stream, frameIndex, groupSize, metadata.FrameFormat, header.BitCount, iconEntries, cancellationToken);
this.EndChunk(stream, frameSizePosition);
frameIndex += groupSize;
}
this.EndChunk(stream, frameListSizePosition);
this.EndChunk(stream, riffSizePosition);
}
/// <summary>
/// Writes the fixed-size ANI animation header chunk.
/// </summary>
/// <param name="stream">The destination stream.</param>
/// <param name="header">The animation header.</param>
private void WriteHeader(Stream stream, AniHeader header)
{
long sizePosition = this.BeginChunk(stream, "anih"u8);
Span<byte> data = this.buffer;
header.WriteTo(data);
stream.Write(data);
this.EndChunk(stream, sizePosition);
}
/// <summary>
/// Writes an identity sequence table when the source metadata declares an explicit sequence.
/// </summary>
/// <param name="stream">The destination stream.</param>
/// <param name="stepCount">The number of animation steps.</param>
private void WriteSequence(Stream stream, int stepCount)
{
long sizePosition = this.BeginChunk(stream, "seq "u8);
Span<byte> value = this.buffer[..sizeof(uint)];
// Decoding expands source resource references into presentation order. Encoding writes those expanded steps as
// distinct resources, so an identity table preserves the explicit-sequence flag without changing playback.
for (uint i = 0; i < stepCount; i++)
{
BinaryPrimitives.WriteUInt32LittleEndian(value, i);
stream.Write(value);
}
this.EndChunk(stream, sizePosition);
}
/// <summary>
/// Writes per-step rates when they cannot be represented by one header value.
/// </summary>
/// <param name="stream">The destination stream.</param>
/// <param name="image">The source image.</param>
/// <param name="displayRate">The default header display rate.</param>
private void WriteRates(Stream stream, Image image, uint displayRate)
{
long sizePosition = this.BeginChunk(stream, "rate"u8);
Span<byte> value = this.buffer[..sizeof(uint)];
// The rate table contains one DWORD per animation step, not one value per resolution variant.
for (int frameIndex = 0; frameIndex < image.Frames.Count;)
{
AniFrameMetadata metadata = image.Frames[frameIndex].Metadata.GetAniMetadata();
uint frameDelay = metadata.FrameDelay;
BinaryPrimitives.WriteUInt32LittleEndian(value, frameDelay is 0 ? displayRate : frameDelay);
stream.Write(value);
frameIndex++;
if (metadata.SequenceNumber > 0)
{
while (frameIndex < image.Frames.Count && image.Frames[frameIndex].Metadata.GetAniMetadata().SequenceNumber == metadata.SequenceNumber)
{
frameIndex++;
}
}
}
this.EndChunk(stream, sizePosition);
}
/// <summary>
/// Writes the optional ANI name and artist list.
/// </summary>
/// <param name="stream">The destination stream.</param>
/// <param name="metadata">The ANI image metadata.</param>
/// <param name="memoryAllocator">The allocator used for the text buffer.</param>
private void WriteInfoList(Stream stream, AniMetadata metadata, MemoryAllocator memoryAllocator)
{
long sizePosition = this.BeginContainer(stream, "LIST"u8, "INFO"u8);
int nameLength = metadata.Name is null ? 0 : Encoding.ASCII.GetByteCount(metadata.Name);
int artistLength = metadata.Artist is null ? 0 : Encoding.ASCII.GetByteCount(metadata.Artist);
// Name and artist are emitted sequentially, so a single buffer sized for the larger value avoids a second allocation.
using IMemoryOwner<byte> owner = memoryAllocator.Allocate<byte>(Math.Max(nameLength, artistLength));
Span<byte> buffer = owner.GetSpan();
if (metadata.Name is not null)
{
this.WriteTextChunk(stream, "INAM"u8, metadata.Name, buffer);
}
if (metadata.Artist is not null)
{
this.WriteTextChunk(stream, "IART"u8, metadata.Artist, buffer);
}
this.EndChunk(stream, sizePosition);
}
/// <summary>
/// Writes a null-terminated ASCII RIFF information chunk.
/// </summary>
/// <param name="stream">The destination stream.</param>
/// <param name="fourCc">The chunk identifier.</param>
/// <param name="value">The text value.</param>
/// <param name="buffer">The reusable text buffer.</param>
private void WriteTextChunk(Stream stream, ReadOnlySpan<byte> fourCc, string value, Span<byte> buffer)
{
long sizePosition = this.BeginChunk(stream, fourCc);
int written = Encoding.ASCII.GetBytes(value, buffer);
stream.Write(buffer[..written]);
// The terminating zero belongs to the RIFF text payload and is therefore included in the backpatched chunk size.
stream.WriteByte(0);
this.EndChunk(stream, sizePosition);
}
/// <summary>
/// Encodes one ANI frame resource using the existing ICO, CUR, or BMP encoder.
/// </summary>
/// <typeparam name="TPixel">The source pixel type.</typeparam>
/// <param name="image">The source image.</param>
/// <param name="stream">The destination stream.</param>
/// <param name="frameIndex">The first source-frame index.</param>
/// <param name="frameCount">The number of source frames in this resource.</param>
/// <param name="format">The embedded resource format.</param>
/// <param name="bitCount">The bitmap bit depth declared by the ANI header.</param>
/// <param name="iconEntries">The reusable icon directory metadata buffer.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void WriteFrameResource<TPixel>(Image<TPixel> image, Stream stream, int frameIndex, int frameCount, AniFrameFormat format, uint bitCount, Span<IconEncoderCore.EncodingFrameMetadata> iconEntries, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
switch (format)
{
case AniFrameFormat.Ico:
case AniFrameFormat.Cur:
// Only the active prefix is exposed to the child encoder; the same backing allocation serves later resources.
Span<IconEncoderCore.EncodingFrameMetadata> entries = iconEntries[..frameCount];
AniIconFrameMetadataProvider provider = new(format);
if (format is AniFrameFormat.Ico)
{
this.icoEncoder ??= new IcoEncoderCore(new IcoEncoder
{
PixelSamplingStrategy = this.encoder.PixelSamplingStrategy,
Quantizer = this.encoder.Quantizer,
SkipMetadata = this.encoder.SkipMetadata,
TransparentColorMode = this.encoder.TransparentColorMode
});
this.icoEncoder.Encode(image, stream, frameIndex, entries, provider, cancellationToken);
}
else
{
this.curEncoder ??= new CurEncoderCore(new CurEncoder
{
PixelSamplingStrategy = this.encoder.PixelSamplingStrategy,
Quantizer = this.encoder.Quantizer,
SkipMetadata = this.encoder.SkipMetadata,
TransparentColorMode = this.encoder.TransparentColorMode
});
this.curEncoder.Encode(image, stream, frameIndex, entries, provider, cancellationToken);
}
break;
case AniFrameFormat.Bmp:
if (this.bmpEncoder is null)
{
BmpEncoder bmpEncoder = new()
{
BitsPerPixel = GetBmpBitsPerPixel(bitCount),
PixelSamplingStrategy = this.encoder.PixelSamplingStrategy,
Quantizer = this.encoder.Quantizer,
SkipFileHeader = true,
SkipMetadata = this.encoder.SkipMetadata,
SupportTransparency = bitCount is 32,
TransparentColorMode = this.encoder.TransparentColorMode
};
this.bmpEncoder = new BmpEncoderCore(bmpEncoder, image.Configuration.MemoryAllocator);
}
// The frame overload writes the raw DIB directly and avoids constructing a temporary single-frame Image.
this.bmpEncoder.Encode(image.Frames[frameIndex], image.Metadata, stream, cancellationToken);
break;
}
}
/// <summary>
/// Creates an icon directory entry from ANI-owned frame metadata.
/// </summary>
/// <param name="metadata">The ANI frame metadata.</param>
/// <param name="format">The embedded icon format.</param>
/// <param name="size">The source frame size.</param>
/// <returns>The icon directory entry.</returns>
private static IconDirEntry CreateIconDirEntry(AniFrameMetadata metadata, AniFrameFormat format, Size size)
{
// PNG and direct-color bitmap entries do not declare a palette; indexed bitmap entries advertise their color count.
byte colorCount = metadata.Compression is IconFrameCompression.Png || metadata.BmpBitsPerPixel > BmpBitsPerPixel.Bit8
? (byte)0
: (byte)ColorNumerics.GetColorCountForBitDepth((int)metadata.BmpBitsPerPixel);
// ICO stores planes/BPP in these fields, while CUR reuses the same two words for the hotspot coordinates.
return new IconDirEntry
{
Width = metadata.EncodingWidth ?? NarrowDimension(size.Width),
Height = metadata.EncodingHeight ?? NarrowDimension(size.Height),
ColorCount = colorCount,
Planes = format is AniFrameFormat.Ico ? (ushort)1 : metadata.HotspotX,
BitCount = format is AniFrameFormat.Ico
? metadata.Compression is IconFrameCompression.Bmp ? (ushort)metadata.BmpBitsPerPixel : (ushort)32
: metadata.HotspotY
};
}
/// <summary>
/// Converts an ANI bitmap bit depth to a supported BMP encoder value.
/// </summary>
/// <param name="bitCount">The ANI bit depth.</param>
/// <returns>The BMP encoder bit depth.</returns>
private static BmpBitsPerPixel GetBmpBitsPerPixel(uint bitCount)
=> bitCount switch
{
1 => BmpBitsPerPixel.Bit1,
2 => BmpBitsPerPixel.Bit2,
4 => BmpBitsPerPixel.Bit4,
8 => BmpBitsPerPixel.Bit8,
16 => BmpBitsPerPixel.Bit16,
24 => BmpBitsPerPixel.Bit24,
_ => BmpBitsPerPixel.Bit32
};
/// <summary>
/// Converts a pixel dimension to the one-byte ICO/CUR representation.
/// </summary>
/// <param name="value">The pixel dimension.</param>
/// <returns>The encoded dimension, where zero represents 256 pixels or greater.</returns>
private static byte NarrowDimension(int value) => value > byte.MaxValue ? (byte)0 : (byte)value;
/// <summary>
/// Begins a RIFF chunk whose size will be backpatched after its payload is written.
/// </summary>
/// <param name="stream">The destination stream.</param>
/// <param name="fourCc">The chunk identifier.</param>
/// <returns>The stream position of the chunk-size field.</returns>
private long BeginChunk(Stream stream, ReadOnlySpan<byte> fourCc)
{
stream.Write(fourCc);
long sizePosition = stream.Position;
// Payload length is unknown until nested encoding completes, so reserve the DWORD and remember its absolute position.
Span<byte> size = this.buffer[..sizeof(uint)];
size.Clear();
stream.Write(size);
return sizePosition;
}
/// <summary>
/// Begins a RIFF container chunk and writes its form or list type.
/// </summary>
/// <param name="stream">The destination stream.</param>
/// <param name="fourCc">The container identifier.</param>
/// <param name="type">The container form or list type.</param>
/// <returns>The stream position of the container-size field.</returns>
private long BeginContainer(Stream stream, ReadOnlySpan<byte> fourCc, ReadOnlySpan<byte> type)
{
long sizePosition = this.BeginChunk(stream, fourCc);
stream.Write(type);
return sizePosition;
}
/// <summary>
/// Word-aligns a RIFF chunk and writes its payload size into the reserved field.
/// </summary>
/// <param name="stream">The destination stream.</param>
/// <param name="sizePosition">The stream position of the reserved size field.</param>
private void EndChunk(Stream stream, long sizePosition)
{
long endPosition = stream.Position;
// sizePosition addresses the size DWORD itself; subtracting its four bytes yields payload length.
uint dataSize = checked((uint)(endPosition - sizePosition - sizeof(uint)));
// RIFF chunk sizes exclude the optional padding byte used to align the next chunk to a WORD boundary.
if ((dataSize & 1) is 1)
{
stream.WriteByte(0);
endPosition++;
}
Span<byte> size = this.buffer[..sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(size, dataSize);
// Backpatch only the reserved DWORD, then restore the append position after any alignment byte.
stream.Position = sizePosition;
stream.Write(size);
stream.Position = endPosition;
}
/// <summary>
/// Projects ANI-owned metadata into the icon encoder without allocating intermediary metadata objects.
/// </summary>
private readonly struct AniIconFrameMetadataProvider : IconEncoderCore.IEncodingFrameMetadataProvider
{
private readonly AniFrameFormat format;
/// <summary>
/// Initializes a new instance of the <see cref="AniIconFrameMetadataProvider"/> struct.
/// </summary>
/// <param name="format">The embedded icon format.</param>
public AniIconFrameMetadataProvider(AniFrameFormat format)
=> this.format = format;
/// <inheritdoc/>
public IconEncoderCore.EncodingFrameMetadata GetEncodingFrameMetadata(ImageFrame frame, out ReadOnlyMemory<Color>? colorTable)
{
AniFrameMetadata metadata = frame.Metadata.GetAniMetadata();
colorTable = metadata.ColorTable;
return new IconEncoderCore.EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, CreateIconDirEntry(metadata, this.format, frame.Size));
}
}
}

40
src/ImageSharp/Formats/Ani/AniFormat.cs

@ -0,0 +1,40 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Describes the ANI image format.
/// </summary>
public sealed class AniFormat : IImageFormat<AniMetadata, AniFrameMetadata>
{
/// <summary>
/// Prevents a default instance of the <see cref="AniFormat"/> class from being created.
/// </summary>
private AniFormat()
{
}
/// <summary>
/// Gets the shared instance.
/// </summary>
public static AniFormat Instance { get; } = new();
/// <inheritdoc/>
public string Name => "ANI";
/// <inheritdoc/>
public string DefaultMimeType => "application/x-navi-animation";
/// <inheritdoc/>
public IEnumerable<string> MimeTypes => AniConstants.MimeTypes;
/// <inheritdoc/>
public IEnumerable<string> FileExtensions => AniConstants.FileExtensions;
/// <inheritdoc/>
public AniMetadata CreateDefaultFormatMetadata() => new();
/// <inheritdoc/>
public AniFrameMetadata CreateDefaultFormatFrameMetadata() => new();
}

25
src/ImageSharp/Formats/Ani/AniFrameFormat.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Specifies the format of the frame data.
/// </summary>
public enum AniFrameFormat : byte
{
/// <summary>
/// The frame resource is encoded as a Windows cursor.
/// </summary>
Cur,
/// <summary>
/// The frame resource is encoded as a Windows icon.
/// </summary>
Ico,
/// <summary>
/// The frame resource is encoded as a Windows bitmap.
/// </summary>
Bmp
}

248
src/ImageSharp/Formats/Ani/AniFrameMetadata.cs

@ -0,0 +1,248 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Provides ANI-specific metadata for an image frame.
/// </summary>
public class AniFrameMetadata : IFormatFrameMetadata<AniFrameMetadata>
{
/// <summary>
/// Initializes a new instance of the <see cref="AniFrameMetadata"/> class.
/// </summary>
public AniFrameMetadata()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AniFrameMetadata"/> class by copying another instance.
/// </summary>
/// <param name="other">The metadata to copy.</param>
private AniFrameMetadata(AniFrameMetadata other)
{
this.FrameDelay = other.FrameDelay;
this.SequenceNumber = other.SequenceNumber;
this.EncodingWidth = other.EncodingWidth;
this.EncodingHeight = other.EncodingHeight;
this.FrameFormat = other.FrameFormat;
this.Compression = other.Compression;
this.BmpBitsPerPixel = other.BmpBitsPerPixel;
this.HotspotX = other.HotspotX;
this.HotspotY = other.HotspotY;
if (other.ColorTable?.Length > 0)
{
this.ColorTable = other.ColorTable.Value.ToArray();
}
}
/// <summary>
/// Gets or sets the frame display time in sixtieths of a second.
/// </summary>
public uint FrameDelay { get; set; }
/// <summary>
/// Gets or sets the animation sequence number.
/// Adjacent frames with the same positive value are grouped as resolution variants in one ANI frame resource.
/// A non-positive value encodes the frame as its own animation step.
/// </summary>
public int SequenceNumber { get; set; }
/// <summary>
/// Gets or sets the encoded frame width.
/// A value of zero represents 256 pixels or greater in ICO and CUR resources.
/// </summary>
public byte? EncodingWidth { get; set; }
/// <summary>
/// Gets or sets the encoded frame height.
/// A value of zero represents 256 pixels or greater in ICO and CUR resources.
/// </summary>
public byte? EncodingHeight { get; set; }
/// <summary>
/// Gets or sets the format used for this frame resource.
/// </summary>
public AniFrameFormat FrameFormat { get; set; }
/// <summary>
/// Gets or sets the embedded ICO or CUR compression format.
/// </summary>
public IconFrameCompression Compression { get; set; } = IconFrameCompression.Png;
/// <summary>
/// Gets or sets the embedded bitmap bits per pixel.
/// </summary>
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32;
/// <summary>
/// Gets or sets the embedded bitmap color table.
/// The underlying pixel format is represented by <see cref="Bgr24"/>.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <summary>
/// Gets or sets the horizontal cursor hotspot in pixels from the left.
/// </summary>
public ushort HotspotX { get; set; }
/// <summary>
/// Gets or sets the vertical cursor hotspot in pixels from the top.
/// </summary>
public ushort HotspotY { get; set; }
/// <inheritdoc/>
public static AniFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata)
{
int bitsPerPixel = metadata.PixelTypeInfo?.BitsPerPixel ?? 32;
BmpBitsPerPixel bmpBitsPerPixel = bitsPerPixel switch
{
1 => BmpBitsPerPixel.Bit1,
2 => BmpBitsPerPixel.Bit2,
<= 4 => BmpBitsPerPixel.Bit4,
<= 8 => BmpBitsPerPixel.Bit8,
<= 16 => BmpBitsPerPixel.Bit16,
<= 24 => BmpBitsPerPixel.Bit24,
_ => BmpBitsPerPixel.Bit32
};
return new AniFrameMetadata
{
FrameDelay = (uint)Math.Round(metadata.Duration.TotalSeconds * 60),
EncodingWidth = ClampEncodingDimension(metadata.EncodingWidth),
EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight),
Compression = bmpBitsPerPixel is BmpBitsPerPixel.Bit32 ? IconFrameCompression.Png : IconFrameCompression.Bmp,
BmpBitsPerPixel = bmpBitsPerPixel
};
}
/// <inheritdoc/>
public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata()
=> new()
{
Duration = TimeSpan.FromSeconds(this.FrameDelay / 60D),
EncodingWidth = this.EncodingWidth,
EncodingHeight = this.EncodingHeight,
PixelTypeInfo = this.GetPixelTypeInfo()
};
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
float ratioX = destination.Width / (float)source.Width;
float ratioY = destination.Height / (float)source.Height;
this.EncodingWidth = ScaleEncodingDimension(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = ScaleEncodingDimension(this.EncodingHeight, destination.Height, ratioY);
this.ColorTable = null;
}
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public AniFrameMetadata DeepClone() => new(this);
/// <summary>
/// Gets the pixel layout represented by the embedded resource metadata.
/// </summary>
/// <returns>The represented pixel layout.</returns>
private PixelTypeInfo GetPixelTypeInfo()
{
int bitsPerPixel = (int)this.BmpBitsPerPixel;
PixelComponentInfo componentInfo;
PixelColorType colorType;
PixelAlphaRepresentation alphaRepresentation = PixelAlphaRepresentation.None;
if (this.Compression is IconFrameCompression.Png)
{
bitsPerPixel = 32;
componentInfo = PixelComponentInfo.Create(4, bitsPerPixel, 8, 8, 8, 8);
colorType = PixelColorType.RGB | PixelColorType.Alpha;
alphaRepresentation = PixelAlphaRepresentation.Unassociated;
}
else
{
switch (this.BmpBitsPerPixel)
{
case BmpBitsPerPixel.Bit1:
componentInfo = PixelComponentInfo.Create(1, bitsPerPixel, 1);
colorType = PixelColorType.Binary;
break;
case BmpBitsPerPixel.Bit2:
componentInfo = PixelComponentInfo.Create(1, bitsPerPixel, 2);
colorType = PixelColorType.Indexed;
break;
case BmpBitsPerPixel.Bit4:
componentInfo = PixelComponentInfo.Create(1, bitsPerPixel, 4);
colorType = PixelColorType.Indexed;
break;
case BmpBitsPerPixel.Bit8:
componentInfo = PixelComponentInfo.Create(1, bitsPerPixel, 8);
colorType = PixelColorType.Indexed;
break;
// Windows bitmaps commonly use a 5-6-5 layout for 16-bit color.
case BmpBitsPerPixel.Bit16:
componentInfo = PixelComponentInfo.Create(3, bitsPerPixel, 5, 6, 5);
colorType = PixelColorType.RGB;
break;
case BmpBitsPerPixel.Bit24:
componentInfo = PixelComponentInfo.Create(3, bitsPerPixel, 8, 8, 8);
colorType = PixelColorType.RGB;
break;
case BmpBitsPerPixel.Bit32 or _:
componentInfo = PixelComponentInfo.Create(4, bitsPerPixel, 8, 8, 8, 8);
colorType = PixelColorType.RGB | PixelColorType.Alpha;
alphaRepresentation = PixelAlphaRepresentation.Unassociated;
break;
}
}
return new PixelTypeInfo(bitsPerPixel)
{
AlphaRepresentation = alphaRepresentation,
ComponentInfo = componentInfo,
ColorType = colorType
};
}
/// <summary>
/// Scales an encoded dimension after an image transform.
/// </summary>
/// <param name="value">The encoded source dimension.</param>
/// <param name="destination">The full destination dimension.</param>
/// <param name="ratio">The destination-to-source scale ratio.</param>
/// <returns>The encoded destination dimension.</returns>
private static byte ScaleEncodingDimension(byte? value, int destination, float ratio)
{
if (value is null)
{
return ClampEncodingDimension(destination);
}
// ICO and CUR encode dimensions in one byte, where zero represents 256 pixels or greater.
int source = value.Value is 0 ? 256 : value.Value;
return ClampEncodingDimension(MathF.Ceiling(source * ratio));
}
/// <summary>
/// Converts a pixel dimension to the one-byte ICO/CUR representation.
/// </summary>
/// <param name="dimension">The pixel dimension.</param>
/// <returns>The encoded dimension.</returns>
private static byte ClampEncodingDimension(float? dimension)
=> dimension switch
{
> 255 => 0,
>= 1 => (byte)dimension,
_ => 0
};
}

114
src/ImageSharp/Formats/Ani/AniFrameStream.cs

@ -0,0 +1,114 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Exposes one ANI frame-resource chunk as an isolated seekable stream.
/// </summary>
/// <remarks>
/// Embedded decoders accept arbitrary seek offsets from their own headers. Bounding those seeks to the
/// current RIFF chunk prevents malformed ICO, CUR, or BMP offsets from reading neighboring ANI chunks.
/// </remarks>
internal sealed class AniFrameStream : Stream
{
private readonly Stream stream;
// start is absolute in the containing stream; position is always relative to this bounded resource.
private long start;
private long length;
private long position;
/// <summary>
/// Initializes a new instance of the <see cref="AniFrameStream"/> class.
/// </summary>
/// <param name="stream">The containing ANI stream.</param>
public AniFrameStream(Stream stream)
=> this.stream = stream;
/// <inheritdoc/>
public override bool CanRead => true;
/// <inheritdoc/>
public override bool CanSeek => true;
/// <inheritdoc/>
public override bool CanWrite => false;
/// <inheritdoc/>
public override long Length => this.length;
/// <inheritdoc/>
public override long Position
{
get => this.position;
set => this.Seek(value, SeekOrigin.Begin);
}
/// <summary>
/// Repositions this stream over another frame-resource payload in the same containing stream.
/// </summary>
/// <param name="start">The absolute start of the frame-resource payload.</param>
/// <param name="length">The frame-resource payload length.</param>
public void Reset(long start, long length)
{
this.start = start;
this.length = length;
this.position = 0;
}
/// <inheritdoc/>
public override void Flush()
{
}
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
=> this.Read(buffer.AsSpan(offset, count));
/// <inheritdoc/>
public override int Read(Span<byte> buffer)
{
// Clamp every read to the resource boundary so a child decoder cannot consume the next RIFF chunk.
int count = (int)Math.Min(buffer.Length, this.length - this.position);
if (count is 0)
{
return 0;
}
// The containing stream is shared by all resources, so synchronize its absolute position immediately before reading.
this.stream.Position = this.start + this.position;
int read = this.stream.Read(buffer[..count]);
this.position += read;
return read;
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin)
{
long target = origin switch
{
SeekOrigin.Begin => offset,
SeekOrigin.Current => this.position + offset,
SeekOrigin.End => this.length + offset,
_ => throw new ArgumentOutOfRangeException(nameof(origin))
};
// Casting rejects both negative offsets and offsets beyond Length with one bounds check.
if ((ulong)target > (ulong)this.length)
{
throw new InvalidImageContentException("The embedded ANI frame resource contains an invalid seek offset.");
}
// Delay moving the containing stream until Read; this keeps logical seeks isolated from sibling resource processing.
this.position = target;
return target;
}
/// <inheritdoc/>
public override void SetLength(long value) => throw new NotSupportedException();
/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}

98
src/ImageSharp/Formats/Ani/AniHeader.cs

@ -0,0 +1,98 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Represents the data stored in an ANI "anih" chunk.
/// </summary>
internal struct AniHeader
{
/// <summary>
/// The number of bytes in the ANI header.
/// </summary>
public const int Size = 9 * sizeof(uint);
/// <summary>
/// Gets or sets the declared ANI header size.
/// </summary>
public uint BytesInHeader { get; set; }
/// <summary>
/// Gets or sets the number of embedded frame resources.
/// </summary>
public uint FrameCount { get; set; }
/// <summary>
/// Gets or sets the number of animation steps.
/// </summary>
public uint StepCount { get; set; }
/// <summary>
/// Gets or sets the frame width used by bitmap-based animations.
/// </summary>
public uint Width { get; set; }
/// <summary>
/// Gets or sets the frame height used by bitmap-based animations.
/// </summary>
public uint Height { get; set; }
/// <summary>
/// Gets or sets the encoded bits per pixel.
/// </summary>
public uint BitCount { get; set; }
/// <summary>
/// Gets or sets the number of color planes.
/// </summary>
public uint Planes { get; set; }
/// <summary>
/// Gets or sets the default display rate in sixtieths of a second.
/// </summary>
public uint DisplayRate { get; set; }
/// <summary>
/// Gets or sets the ANI header flags.
/// </summary>
public AniHeaderFlags Flags { get; set; }
/// <summary>
/// Parses an ANI header from its little-endian byte representation.
/// </summary>
/// <param name="data">The ANI header data.</param>
/// <returns>The parsed ANI header.</returns>
public static AniHeader Parse(ReadOnlySpan<byte> data)
=> new()
{
BytesInHeader = BinaryPrimitives.ReadUInt32LittleEndian(data),
FrameCount = BinaryPrimitives.ReadUInt32LittleEndian(data[4..]),
StepCount = BinaryPrimitives.ReadUInt32LittleEndian(data[8..]),
Width = BinaryPrimitives.ReadUInt32LittleEndian(data[12..]),
Height = BinaryPrimitives.ReadUInt32LittleEndian(data[16..]),
BitCount = BinaryPrimitives.ReadUInt32LittleEndian(data[20..]),
Planes = BinaryPrimitives.ReadUInt32LittleEndian(data[24..]),
DisplayRate = BinaryPrimitives.ReadUInt32LittleEndian(data[28..]),
Flags = (AniHeaderFlags)BinaryPrimitives.ReadUInt32LittleEndian(data[32..])
};
/// <summary>
/// Writes the ANI header to its little-endian byte representation.
/// </summary>
/// <param name="destination">The destination buffer.</param>
public readonly void WriteTo(Span<byte> destination)
{
BinaryPrimitives.WriteUInt32LittleEndian(destination, this.BytesInHeader);
BinaryPrimitives.WriteUInt32LittleEndian(destination[4..], this.FrameCount);
BinaryPrimitives.WriteUInt32LittleEndian(destination[8..], this.StepCount);
BinaryPrimitives.WriteUInt32LittleEndian(destination[12..], this.Width);
BinaryPrimitives.WriteUInt32LittleEndian(destination[16..], this.Height);
BinaryPrimitives.WriteUInt32LittleEndian(destination[20..], this.BitCount);
BinaryPrimitives.WriteUInt32LittleEndian(destination[24..], this.Planes);
BinaryPrimitives.WriteUInt32LittleEndian(destination[28..], this.DisplayRate);
BinaryPrimitives.WriteUInt32LittleEndian(destination[32..], (uint)this.Flags);
}
}

21
src/ImageSharp/Formats/Ani/AniHeaderFlags.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Flags for the ANI header.
/// </summary>
[Flags]
public enum AniHeaderFlags : uint
{
/// <summary>
/// The "icon" chunks contain ICO or CUR resources. Without this flag, they contain BMP resources.
/// </summary>
IsIcon = 1,
/// <summary>
/// The ANI file contains a "seq " chunk that maps animation steps to frame resources.
/// </summary>
ContainsSequence = 2
}

39
src/ImageSharp/Formats/Ani/AniImageFormatDetector.cs

@ -0,0 +1,39 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Detects ANI file headers.
/// </summary>
public sealed class AniImageFormatDetector : IImageFormatDetector
{
/// <summary>
/// Initializes a new instance of the <see cref="AniImageFormatDetector"/> class.
/// </summary>
public AniImageFormatDetector()
{
}
/// <inheritdoc/>
public int HeaderSize => AniConstants.RiffHeaderSize;
/// <inheritdoc/>
public bool TryDetectFormat(ReadOnlySpan<byte> header, [NotNullWhen(true)] out IImageFormat? format)
{
format = this.IsSupportedFileFormat(header) ? AniFormat.Instance : null;
return format is not null;
}
/// <summary>
/// Determines whether the supplied header is a RIFF container with the ANI "ACON" form type.
/// </summary>
/// <param name="header">The candidate file header.</param>
/// <returns><see langword="true"/> when the header identifies ANI data.</returns>
private bool IsSupportedFileFormat(ReadOnlySpan<byte> header)
=> header.Length >= this.HeaderSize
&& header[..4].SequenceEqual(AniConstants.RiffFourCc)
&& header.Slice(8, 4).SequenceEqual(AniConstants.AniFormTypeFourCc);
}

134
src/ImageSharp/Formats/Ani/AniMetadata.cs

@ -0,0 +1,134 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Provides ANI-specific metadata for an image.
/// </summary>
public class AniMetadata : IFormatMetadata<AniMetadata>
{
/// <summary>
/// Initializes a new instance of the <see cref="AniMetadata"/> class.
/// </summary>
public AniMetadata()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AniMetadata"/> class by copying another instance.
/// </summary>
/// <param name="other">The metadata to copy.</param>
private AniMetadata(AniMetadata other)
{
this.Width = other.Width;
this.Height = other.Height;
this.BitCount = other.BitCount;
this.Planes = other.Planes;
this.DisplayRate = other.DisplayRate;
this.Flags = other.Flags;
this.Name = other.Name;
this.Artist = other.Artist;
}
/// <summary>
/// Gets or sets the frame width declared by the ANI header.
/// </summary>
/// <remarks>
/// Icon-based ANI files commonly store zero because each embedded resource declares its own dimensions.
/// </remarks>
public uint Width { get; set; }
/// <summary>
/// Gets or sets the frame height declared by the ANI header.
/// </summary>
/// <remarks>
/// Icon-based ANI files commonly store zero because each embedded resource declares its own dimensions.
/// </remarks>
public uint Height { get; set; }
/// <summary>
/// Gets or sets the bits per pixel declared by the ANI header.
/// </summary>
/// <remarks>
/// Bitmap-based ANI files use this value to describe their raw frame data. Icon-based files commonly store zero
/// because each embedded ICO or CUR entry declares its own pixel layout.
/// </remarks>
public uint BitCount { get; set; }
/// <summary>
/// Gets or sets the number of independently addressable color planes declared by the ANI header.
/// </summary>
/// <remarks>
/// Bitmap-based ANI files use the Windows DIB plane value, which must be one. Icon-based ANI files use zero because
/// each embedded ICO or CUR entry describes its own pixel layout. No other values are defined by the format.
/// </remarks>
public uint Planes { get; set; }
/// <summary>
/// Gets or sets the default frame display rate in sixtieths of a second.
/// </summary>
public uint DisplayRate { get; set; }
/// <summary>
/// Gets or sets the ANI header flags.
/// </summary>
public AniHeaderFlags Flags { get; set; } = AniHeaderFlags.IsIcon;
/// <summary>
/// Gets or sets the animation name.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Gets or sets the animation artist.
/// </summary>
public string? Artist { get; set; }
/// <inheritdoc/>
public static AniMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
=> new()
{
BitCount = (uint)metadata.PixelTypeInfo.BitsPerPixel,
Planes = 1,
Flags = AniHeaderFlags.IsIcon
};
/// <inheritdoc/>
public PixelTypeInfo GetPixelTypeInfo()
{
// Icon-based files are allowed to leave the global bit depth unspecified. Their embedded
// ICO/CUR metadata carries the exact value, while 32-bit is the least lossy conversion default.
int bitsPerPixel = this.BitCount is > 0 and <= 32 ? (int)this.BitCount : 32;
return new PixelTypeInfo(bitsPerPixel);
}
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
=> new()
{
AnimateRootFrame = true,
EncodingType = EncodingType.Lossless,
PixelTypeInfo = this.GetPixelTypeInfo()
};
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
if (!this.Flags.HasFlag(AniHeaderFlags.IsIcon))
{
this.Width = (uint)destination.Width;
this.Height = (uint)destination.Height;
}
}
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public AniMetadata DeepClone() => new(this);
}

34
src/ImageSharp/Formats/Ani/AniRiffChunkHeader.cs

@ -0,0 +1,34 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Represents a RIFF chunk identifier and payload size.
/// </summary>
internal struct AniRiffChunkHeader
{
/// <summary>
/// Gets or sets the chunk identifier.
/// </summary>
public uint FourCc { get; set; }
/// <summary>
/// Gets or sets the chunk payload size in bytes, excluding alignment padding.
/// </summary>
public uint Size { get; set; }
/// <summary>
/// Parses a RIFF chunk header from its little-endian byte representation.
/// </summary>
/// <param name="data">The RIFF chunk header data.</param>
/// <returns>The parsed RIFF chunk header.</returns>
public static AniRiffChunkHeader Parse(ReadOnlySpan<byte> data)
=> new()
{
FourCc = BinaryPrimitives.ReadUInt32LittleEndian(data),
Size = BinaryPrimitives.ReadUInt32LittleEndian(data[4..])
};
}

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

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Bmp;

71
src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

@ -131,6 +131,7 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
try
{
int bytesPerColorMapEntry = this.ReadImageHeaders(stream, out bool inverted, out byte[] palette);
ushort bitsPerPixel = this.infoHeader.BitsPerPixel;
image = new Image<TPixel>(this.configuration, this.infoHeader.Width, this.infoHeader.Height, this.metadata);
@ -138,23 +139,27 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
switch (this.infoHeader.Compression)
{
case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 32 && this.bmpMetadata.InfoHeaderType is BmpInfoHeaderType.WinVersion3:
case BmpCompression.RGB when bitsPerPixel is 32 && this.bmpMetadata.InfoHeaderType is BmpInfoHeaderType.WinVersion3:
this.ReadRgb32Slow(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
break;
case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 32:
case BmpCompression.RGB when bitsPerPixel is 32:
this.ReadRgb32Fast(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
break;
case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 24:
case BmpCompression.RGB when bitsPerPixel is 24:
this.ReadRgb24(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
break;
case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 16:
case BmpCompression.RGB when bitsPerPixel is 16:
this.ReadRgb16(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
break;
case BmpCompression.RGB when this.infoHeader.BitsPerPixel is <= 8 && this.processedAlphaMask:
case BmpCompression.RGB when bitsPerPixel is > 0 and <= 8 && this.processedAlphaMask:
this.ReadRgbPaletteWithAlphaMask(
stream,
pixels,
@ -166,7 +171,8 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
inverted);
break;
case BmpCompression.RGB when this.infoHeader.BitsPerPixel is <= 8:
case BmpCompression.RGB when bitsPerPixel is > 0 and <= 8:
this.ReadRgbPalette(
stream,
pixels,
@ -179,6 +185,10 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
break;
case BmpCompression.RGB when bitsPerPixel is <= 0 or > 32:
BmpThrowHelper.ThrowInvalidImageContentException($"Invalid bits per pixel: {bitsPerPixel}");
break;
case BmpCompression.RLE24:
this.ReadRle24(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
@ -340,10 +350,10 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
pixelRow[x] = this.rleSkippedPixelHandling switch
{
RleSkippedPixelHandling.FirstColorOfPalette => TPixel.FromBgr24(Unsafe.As<byte, Bgr24>(ref colors[colorIdx * 4])),
RleSkippedPixelHandling.Transparent => TPixel.FromScaledVector4(Vector4.Zero),
RleSkippedPixelHandling.Transparent => TPixel.FromUnassociatedScaledVector4(Vector4.Zero),
// Default handling for skipped pixels is black (which is what System.Drawing is also doing).
_ => TPixel.FromScaledVector4(new Vector4(0.0f, 0.0f, 0.0f, 1.0f)),
_ => TPixel.FromUnassociatedScaledVector4(new Vector4(0.0f, 0.0f, 0.0f, 1.0f)),
};
}
else
@ -401,10 +411,10 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
pixelRow[x] = this.rleSkippedPixelHandling switch
{
RleSkippedPixelHandling.FirstColorOfPalette => TPixel.FromBgr24(Unsafe.As<byte, Bgr24>(ref bufferSpan[idx])),
RleSkippedPixelHandling.Transparent => TPixel.FromScaledVector4(Vector4.Zero),
RleSkippedPixelHandling.Transparent => TPixel.FromUnassociatedScaledVector4(Vector4.Zero),
// Default handling for skipped pixels is black (which is what System.Drawing is also doing).
_ => TPixel.FromScaledVector4(new Vector4(0.0f, 0.0f, 0.0f, 1.0f)),
_ => TPixel.FromUnassociatedScaledVector4(new Vector4(0.0f, 0.0f, 0.0f, 1.0f)),
};
}
else
@ -1262,7 +1272,7 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
g * invMaxValueGreen,
b * invMaxValueBlue,
alpha);
pixelRow[x] = TPixel.FromScaledVector4(vector4);
pixelRow[x] = TPixel.FromUnassociatedScaledVector4(vector4);
}
else
{
@ -1421,12 +1431,8 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
this.infoHeader = BmpInfoHeader.ParseV5(buffer);
if (this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0)
{
// Read color profile.
long streamPosition = stream.Position;
byte[] iccProfileData = new byte[this.infoHeader.ProfileSize];
stream.Position = infoHeaderStart + this.infoHeader.ProfileData;
stream.Read(iccProfileData);
this.metadata.IccProfile = new IccProfile(iccProfileData);
this.ExecuteAncillarySegmentAction(() => this.ReadIccProfile(stream, this.metadata, infoHeaderStart));
stream.Position = streamPosition;
}
}
@ -1460,6 +1466,33 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
this.Dimensions = new Size(this.infoHeader.Width, this.infoHeader.Height);
}
/// <summary>
/// Reads the embedded ICC profile from the BMP V5 info header.
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="imageMetadata">The image metadata.</param>
/// <param name="infoHeaderStart">The stream position where the info header begins.</param>
private void ReadIccProfile(BufferedReadStream stream, ImageMetadata imageMetadata, long infoHeaderStart)
{
byte[] iccProfileData = new byte[this.infoHeader.ProfileSize];
stream.Position = infoHeaderStart + this.infoHeader.ProfileData;
if (stream.Read(iccProfileData) != iccProfileData.Length)
{
BmpThrowHelper.ThrowInvalidImageContentException("Not enough data to read BMP ICC profile.");
}
IccProfile profile = new(iccProfileData);
if (profile.CheckIsValid())
{
imageMetadata.IccProfile = profile;
}
else
{
throw new InvalidIccProfileException("Invalid BMP ICC profile.");
}
}
/// <summary>
/// Reads the <see cref="BmpFileHeader"/> from the stream.
/// </summary>
@ -1538,6 +1571,12 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
case BmpFileMarkerType.Bitmap:
if (this.fileHeader.HasValue)
{
if (this.fileHeader.Value.Offset > stream.Length)
{
BmpThrowHelper.ThrowInvalidImageContentException(
$"Pixel data offset {this.fileHeader.Value.Offset} exceeds file size {stream.Length}.");
}
colorMapSizeBytes = this.fileHeader.Value.Offset - BmpFileHeader.Size - this.infoHeader.HeaderSize;
}
else

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

@ -13,7 +13,7 @@ public sealed class BmpEncoder : QuantizingImageEncoder
/// <summary>
/// Initializes a new instance of the <see cref="BmpEncoder"/> class.
/// </summary>
public BmpEncoder() => this.Quantizer = KnownQuantizers.Octree;
public BmpEncoder() => this.Quantizer = KnownQuantizers.Hexadecatree;
/// <summary>
/// Gets the number of bits per pixel.

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

@ -102,6 +102,11 @@ internal sealed class BmpEncoderCore
/// <inheritdoc cref="BmpDecoderOptions.SkipFileHeader"/>
private readonly bool skipFileHeader;
/// <summary>
/// Whether optional image metadata should be omitted.
/// </summary>
private readonly bool skipMetadata;
/// <inheritdoc cref="BmpDecoderOptions.UseDoubleHeight"/>
private readonly bool isDoubleHeight;
@ -116,12 +121,13 @@ internal sealed class BmpEncoderCore
this.bitsPerPixel = encoder.BitsPerPixel;
// TODO: Use a palette quantizer if supplied.
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree;
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Hexadecatree;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.transparentColorMode = encoder.TransparentColorMode;
this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3;
this.processedAlphaMask = encoder.ProcessedAlphaMask;
this.skipFileHeader = encoder.SkipFileHeader;
this.skipMetadata = encoder.SkipMetadata;
this.isDoubleHeight = encoder.UseDoubleHeight;
}
@ -138,17 +144,30 @@ internal sealed class BmpEncoderCore
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
// Stream may not at 0.
this.Encode(image.Frames.RootFrame, image.Metadata, stream, cancellationToken);
}
/// <summary>
/// Encodes a source frame using the supplied image metadata.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The source frame.</param>
/// <param name="metadata">The source image metadata.</param>
/// <param name="stream">The destination stream.</param>
/// <param name="cancellationToken">The token to request cancellation.</param>
internal void Encode<TPixel>(ImageFrame<TPixel> frame, ImageMetadata metadata, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
// Nested ANI/ICO/CUR encoding starts inside a parent stream, so all later profile offsets use this local base.
long basePosition = stream.Position;
Configuration configuration = image.Configuration;
ImageMetadata metadata = image.Metadata;
Configuration configuration = frame.Configuration;
BmpMetadata bmpMetadata = metadata.GetBmpMetadata();
this.bitsPerPixel ??= bmpMetadata.BitsPerPixel;
ushort bpp = (ushort)this.bitsPerPixel;
int bytesPerLine = (int)(4 * ((((uint)image.Width * bpp) + 31) / 32));
this.padding = bytesPerLine - (int)(image.Width * (bpp / 8F));
int bytesPerLine = (int)(4 * ((((uint)frame.Width * bpp) + 31) / 32));
this.padding = bytesPerLine - (int)(frame.Width * (bpp / 8F));
int colorPaletteSize = this.bitsPerPixel switch
{
@ -161,7 +180,7 @@ internal sealed class BmpEncoderCore
byte[]? iccProfileData = null;
int iccProfileSize = 0;
if (metadata.IccProfile != null)
if (!this.skipMetadata && metadata.IccProfile != null)
{
this.infoHeaderType = BmpInfoHeaderType.WinVersion5;
iccProfileData = metadata.IccProfile.ToByteArray();
@ -176,25 +195,25 @@ internal sealed class BmpEncoderCore
_ => BmpInfoHeader.SizeV3
};
// for ico/cur encoder.
int height = image.Height;
// ICO/CUR DIB headers include the XOR bitmap and following AND mask in one doubled height.
int height = frame.Height;
if (this.isDoubleHeight)
{
height <<= 1;
}
BmpInfoHeader infoHeader = this.CreateBmpInfoHeader(image.Width, height, infoHeaderSize, bpp, bytesPerLine, metadata, iccProfileData);
BmpInfoHeader infoHeader = this.CreateBmpInfoHeader(frame.Width, height, infoHeaderSize, bpp, bytesPerLine, metadata, iccProfileData);
Span<byte> buffer = stackalloc byte[infoHeaderSize];
// For ico/cur encoder.
// ICO/CUR resources contain a DIB directly; standalone BMP files additionally require BITMAPFILEHEADER.
if (!this.skipFileHeader)
{
WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer);
}
this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize);
this.WriteImage(configuration, stream, image, cancellationToken);
this.WriteImage(configuration, stream, frame, cancellationToken);
WriteColorProfile(stream, iccProfileData, buffer, basePosition);
stream.Flush();
@ -216,7 +235,8 @@ internal sealed class BmpEncoderCore
int hResolution = 0;
int vResolution = 0;
if (metadata.ResolutionUnits != PixelResolutionUnit.AspectRatio
if (!this.skipMetadata
&& metadata.ResolutionUnits != PixelResolutionUnit.AspectRatio
&& metadata.HorizontalResolution > 0
&& metadata.VerticalResolution > 0)
{
@ -348,15 +368,11 @@ internal sealed class BmpEncoderCore
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image">
/// <param name="frame">
/// The <see cref="ImageFrame{TPixel}"/> containing pixel data.
/// </param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void WriteImage<TPixel>(
Configuration configuration,
Stream stream,
Image<TPixel> image,
CancellationToken cancellationToken)
private void WriteImage<TPixel>(Configuration configuration, Stream stream, ImageFrame<TPixel> frame, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
ImageFrame<TPixel>? clonedFrame = null;
@ -367,11 +383,11 @@ internal sealed class BmpEncoderCore
int bpp = this.bitsPerPixel != null ? (int)this.bitsPerPixel : 32;
if (bpp > 8 && EncodingUtilities.ShouldReplaceTransparentPixels<TPixel>(this.transparentColorMode))
{
clonedFrame = image.Frames.RootFrame.Clone();
clonedFrame = frame.Clone();
EncodingUtilities.ReplaceTransparentPixels(clonedFrame);
}
ImageFrame<TPixel> encodingFrame = clonedFrame ?? image.Frames.RootFrame;
ImageFrame<TPixel> encodingFrame = clonedFrame ?? frame;
Buffer2D<TPixel> pixels = encodingFrame.PixelBuffer;
switch (this.bitsPerPixel)
@ -864,10 +880,17 @@ internal sealed class BmpEncoderCore
stream.WriteByte(indices);
}
/// <summary>
/// Writes the bottom-up 1-bit transparency mask required by an ICO/CUR bitmap resource.
/// </summary>
/// <typeparam name="TPixel">The source pixel type.</typeparam>
/// <param name="stream">The destination stream.</param>
/// <param name="encodingFrame">The source frame.</param>
private static void ProcessedAlphaMask<TPixel>(Stream stream, ImageFrame<TPixel> encodingFrame)
where TPixel : unmanaged, IPixel<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
int arrayWidth = encodingFrame.Width / 8;
// Each byte represents eight pixels and every scanline is padded to a 4-byte DIB boundary.
int arrayWidth = (encodingFrame.Width + 7) / 8;
int padding = arrayWidth % 4;
if (padding is not 0)
{
@ -875,6 +898,9 @@ internal sealed class BmpEncoderCore
}
Span<byte> mask = stackalloc byte[arrayWidth];
Span<byte> paddingBytes = stackalloc byte[3];
paddingBytes.Clear();
for (int y = encodingFrame.Height - 1; y >= 0; y--)
{
mask.Clear();
@ -884,19 +910,30 @@ internal sealed class BmpEncoderCore
{
int x = i * 8;
for (int j = 0; j < 8; j++)
// The final byte can represent fewer than eight pixels when the image width is not byte-aligned.
int pixelCount = Math.Min(8, encodingFrame.Width - x);
for (int j = 0; j < pixelCount; j++)
{
WriteAlphaMask(row[x + j], ref mask[i], j);
}
}
stream.Write(mask);
stream.Skip(padding);
// Alpha-mask rows are DWORD-aligned, and the final row padding must extend the stream.
stream.Write(paddingBytes[..padding]);
}
}
/// <summary>
/// Sets one most-significant-bit-first transparency flag in an ICO/CUR AND-mask byte.
/// </summary>
/// <typeparam name="TPixel">The source pixel type.</typeparam>
/// <param name="pixel">The source pixel.</param>
/// <param name="mask">The destination mask byte.</param>
/// <param name="index">The pixel index within the byte.</param>
private static void WriteAlphaMask<TPixel>(in TPixel pixel, ref byte mask, in int index)
where TPixel : unmanaged, IPixel<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
Rgba32 rgba = pixel.ToRgba32();
if (rgba.A is 0)

6
src/ImageSharp/Formats/Cur/CurConfigurationModule.cs

@ -1,12 +1,10 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Icon;
namespace SixLabors.ImageSharp.Formats.Cur;
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the Ico format.
/// Registers the image encoder, decoder, and format detector for the CUR format.
/// </summary>
public sealed class CurConfigurationModule : IImageFormatConfigurationModule
{
@ -15,6 +13,6 @@ public sealed class CurConfigurationModule : IImageFormatConfigurationModule
{
configuration.ImageFormatsManager.SetEncoder(CurFormat.Instance, new CurEncoder());
configuration.ImageFormatsManager.SetDecoder(CurFormat.Instance, CurDecoder.Instance);
configuration.ImageFormatsManager.AddImageFormatDetector(new IconImageFormatDetector());
configuration.ImageFormatsManager.AddImageFormatDetector(new CurImageFormatDetector());
}
}

10
src/ImageSharp/Formats/Cur/CurConstants.cs

@ -4,12 +4,12 @@
namespace SixLabors.ImageSharp.Formats.Cur;
/// <summary>
/// Defines constants relating to ICOs
/// Defines constants used by the CUR format.
/// </summary>
internal static class CurConstants
{
/// <summary>
/// The list of mime types that equate to a cur.
/// The MIME types that identify CUR data.
/// </summary>
/// <remarks>
/// See <see href="https://en.wikipedia.org/wiki/ICO_(file_format)#MIME_type"/>
@ -27,13 +27,11 @@ internal static class CurConstants
"image/ico",
"image/icon",
"text/ico",
"application/ico",
"application/ico"
];
/// <summary>
/// The list of file extensions that equate to a cur.
/// The file extensions that identify CUR data.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = ["cur"];
public const uint FileHeader = 0x00_02_00_00;
}

7
src/ImageSharp/Formats/Cur/CurDecoder.cs

@ -6,10 +6,13 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Cur;
/// <summary>
/// Decoder for generating an image out of a ico encoded stream.
/// Decoder for generating an image from a CUR encoded stream.
/// </summary>
public sealed class CurDecoder : ImageDecoder
{
/// <summary>
/// Initializes a new instance of the <see cref="CurDecoder"/> class.
/// </summary>
private CurDecoder()
{
}
@ -34,7 +37,7 @@ public sealed class CurDecoder : ImageDecoder
/// <inheritdoc/>
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
=> this.Decode<Rgba32>(options, stream, cancellationToken);
=> this.Decode<Rgba32>(options, stream, cancellationToken);
/// <inheritdoc/>
protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken)

10
src/ImageSharp/Formats/Cur/CurDecoderCore.cs

@ -7,13 +7,21 @@ using SixLabors.ImageSharp.Metadata;
namespace SixLabors.ImageSharp.Formats.Cur;
/// <summary>
/// Decodes CUR containers and maps directory metadata to CUR metadata.
/// </summary>
internal sealed class CurDecoderCore : IconDecoderCore
{
/// <summary>
/// Initializes a new instance of the <see cref="CurDecoderCore"/> class.
/// </summary>
/// <param name="options">The decoder options.</param>
public CurDecoderCore(DecoderOptions options)
: base(options)
: base(options, IconFileType.CUR)
{
}
/// <inheritdoc/>
protected override void SetFrameMetadata(
ImageMetadata imageMetadata,
ImageFrameMetadata frameMetadata,

5
src/ImageSharp/Formats/Cur/CurEncoder.cs

@ -10,8 +10,5 @@ public sealed class CurEncoder : QuantizingImageEncoder
{
/// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
CurEncoderCore encoderCore = new(this);
encoderCore.Encode(image, stream, cancellationToken);
}
=> new CurEncoderCore(this).Encode(image, stream, cancellationToken);
}

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

Loading…
Cancel
Save