diff --git a/.gitattributes b/.gitattributes
index f7bd4d061..a0643702b 100644
--- a/.gitattributes
+++ b/.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
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 000000000..5f9f69435
--- /dev/null
+++ b/.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.
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index e00757cb7..bb0962784 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.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
diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml
index a7278a817..eca2f190f 100644
--- a/.github/workflows/code-coverage.yml
+++ b/.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 }}
diff --git a/.gitignore b/.gitignore
index fadf36964..a8d2917be 100644
--- a/.gitignore
+++ b/.gitignore
@@ -227,3 +227,5 @@ artifacts/
#lfs
hooks/**
lfs/**
+
+.dotnet
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 000000000..814e3ce26
--- /dev/null
+++ b/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.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 000000000..5f08a449a
--- /dev/null
+++ b/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.
diff --git a/Directory.Build.props b/Directory.Build.props
index 755cbe3b3..ab898020c 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -21,10 +21,6 @@
-
- 12.0
-
-
- 4.0
+
+ 5.0
- net8.0;net9.0
+ net10.0;net11.0
- net8.0
+ net10.0
@@ -47,7 +47,8 @@
-
+
+
@@ -61,6 +62,11 @@
True
InlineArray.tt
+
+ True
+ True
+ ImageExtensions.Save.tt
+
True
True
@@ -141,15 +147,20 @@
True
DefaultPixelBlenders.Generated.tt
-
+
True
True
- PorterDuffFunctions.Generated.tt
+ AssociatedAlphaPixelBlenders.Generated.tt
-
+
+ True
True
+ AssociatedAlphaPorterDuffFunctions.Generated.tt
+
+
True
- ImageExtensions.Save.tt
+ True
+ PorterDuffFunctions.Generated.tt
@@ -230,6 +241,14 @@
DefaultPixelBlenders.Generated.cs
TextTemplatingFileGenerator
+
+ AssociatedAlphaPixelBlenders.Generated.cs
+ TextTemplatingFileGenerator
+
+
+ AssociatedAlphaPorterDuffFunctions.Generated.cs
+ TextTemplatingFileGenerator
+
TextTemplatingFileGenerator
ImageExtensions.Save.cs
diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs
index dff8f577f..4881518c9 100644
--- a/src/ImageSharp/Image{TPixel}.cs
+++ b/src/ImageSharp/Image{TPixel}.cs
@@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
@@ -91,7 +90,7 @@ public sealed class Image : Image
Configuration configuration,
Buffer2D pixelBuffer,
ImageMetadata metadata)
- : this(configuration, pixelBuffer.FastMemoryGroup, pixelBuffer.Width, pixelBuffer.Height, metadata)
+ : this(configuration, pixelBuffer.FastMemoryGroup, pixelBuffer.Width, pixelBuffer.Height, pixelBuffer.RowStride, metadata)
{
}
@@ -110,8 +109,29 @@ public sealed class Image : Image
int width,
int height,
ImageMetadata metadata)
+ : this(configuration, memoryGroup, width, height, width, metadata)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class
+ /// wrapping an external .
+ ///
+ /// The configuration providing initialization code which allows extending the library.
+ /// The memory source.
+ /// The width of the image in pixels.
+ /// The height of the image in pixels.
+ /// The number of elements between row starts.
+ /// The images metadata.
+ internal Image(
+ Configuration configuration,
+ MemoryGroup memoryGroup,
+ int width,
+ int height,
+ int rowStride,
+ ImageMetadata metadata)
: base(configuration, TPixel.GetPixelTypeInfo(), metadata, width, height)
- => this.frames = new ImageFrameCollection(this, width, height, memoryGroup);
+ => this.frames = new ImageFrameCollection(this, width, height, rowStride, memoryGroup);
///
/// Initializes a new instance of the class
@@ -287,16 +307,24 @@ public sealed class Image : Image
}
///
- /// Copy image pixels to .
+ /// Copy image pixels to using the root frame backing row layout.
///
+ ///
+ /// Destination length must be at least
+ /// ((Height - 1) * Frames.RootFrame.PixelBuffer.RowStride) + Width.
+ ///
/// The to copy image pixels to.
- public void CopyPixelDataTo(Span destination) => this.GetPixelMemoryGroup().CopyTo(destination);
+ public void CopyPixelDataTo(Span destination) => this.Frames.RootFrame.CopyPixelDataTo(destination);
///
- /// Copy image pixels to .
+ /// Copy image pixels to using the root frame backing row layout.
///
+ ///
+ /// Destination length must be at least
+ /// (((Height - 1) * Frames.RootFrame.PixelBuffer.RowStride) + Width) * sizeof(TPixel) bytes.
+ ///
/// The of to copy image pixels to.
- public void CopyPixelDataTo(Span destination) => this.GetPixelMemoryGroup().CopyTo(MemoryMarshal.Cast(destination));
+ public void CopyPixelDataTo(Span destination) => this.Frames.RootFrame.CopyPixelDataTo(destination);
///
/// Gets the representation of the pixels as a in the source image's pixel format
@@ -311,17 +339,7 @@ public sealed class Image : Image
/// The referencing the image buffer.
/// The indicating the success.
public bool DangerousTryGetSinglePixelMemory(out Memory memory)
- {
- IMemoryGroup mg = this.GetPixelMemoryGroup();
- if (mg.Count > 1)
- {
- memory = default;
- return false;
- }
-
- memory = mg.Single();
- return true;
- }
+ => this.Frames.RootFrame.DangerousTryGetSinglePixelMemory(out memory);
///
/// Clones the current image.
diff --git a/src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs b/src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs
new file mode 100644
index 000000000..764bb37e8
--- /dev/null
+++ b/src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs
@@ -0,0 +1,67 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+
+namespace SixLabors.ImageSharp.Memory;
+
+///
+/// Provides the tracked memory-owner contract required by .
+///
+/// The element type.
+///
+/// Custom allocators implement
+/// and return a derived type. The base allocator attaches allocation tracking after the owner has been
+/// created so custom implementations cannot forget, duplicate, or mismatch the reservation lifecycle.
+///
+public abstract class AllocationTrackedMemoryManager : MemoryManager
+ where T : struct
+{
+ private AllocationTrackingState allocationTracking;
+
+ ///
+ /// Releases resources held by the concrete tracked owner.
+ ///
+ ///
+ /// when the owner is being disposed deterministically;
+ /// otherwise, .
+ ///
+ ///
+ /// Implementations release their own resources here. Allocation tracking is released by the sealed base
+ /// dispose path after this method returns.
+ ///
+ protected abstract void DisposeCore(bool disposing);
+
+ ///
+ protected sealed override void Dispose(bool disposing)
+ {
+ try
+ {
+ this.DisposeCore(disposing);
+ }
+ finally
+ {
+ this.ReleaseAllocationTracking();
+ }
+ }
+
+ ///
+ /// Attaches allocation tracking to this owner after allocation has succeeded.
+ ///
+ /// The allocator that owns the reservation for this instance.
+ /// The reserved allocation size, in bytes.
+ ///
+ /// calls this exactly once after AllocateCore returns.
+ /// Derived allocators should not call it themselves; they only construct the concrete owner.
+ ///
+ protected internal virtual void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes)
+ => this.allocationTracking.Attach(allocator, lengthInBytes);
+
+ ///
+ /// Releases any tracked allocation bytes associated with this instance.
+ ///
+ ///
+ /// Calling this more than once is safe; only the first call after tracking has been attached releases bytes.
+ ///
+ private void ReleaseAllocationTracking() => this.allocationTracking.Release();
+}
diff --git a/src/ImageSharp/Memory/AllocationTrackingState.cs b/src/ImageSharp/Memory/AllocationTrackingState.cs
new file mode 100644
index 000000000..1e9a632ed
--- /dev/null
+++ b/src/ImageSharp/Memory/AllocationTrackingState.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Memory;
+
+///
+/// Tracks a single allocator reservation and releases it exactly once.
+///
+///
+/// This type is intended to live as a mutable field on the owning object. It should not be copied
+/// after tracking has been attached, because the owner relies on a single shared release state.
+///
+internal struct AllocationTrackingState
+{
+ private MemoryAllocator? allocator;
+ private long lengthInBytes;
+ private int released;
+
+ ///
+ /// Attaches allocator reservation tracking to the current owner.
+ ///
+ /// The allocator that owns the reservation.
+ /// The reserved allocation size, in bytes.
+ ///
+ /// Must complete-before the owning object's reference is observable to any other thread.
+ /// guarantees this by attaching synchronously on the allocating
+ /// thread before returning the owner; reference publication then provides the release fence
+ /// that makes these field writes visible to a subsequent on another thread.
+ ///
+ internal void Attach(MemoryAllocator allocator, long lengthInBytes)
+ {
+ this.allocator = allocator;
+ this.lengthInBytes = lengthInBytes;
+ }
+
+ ///
+ /// Releases the attached allocator reservation once.
+ ///
+ internal void Release()
+ {
+ if (Interlocked.Exchange(ref this.released, 1) == 0 && this.allocator != null)
+ {
+ this.allocator.ReleaseAccumulatedBytes(this.lengthInBytes);
+ this.allocator = null;
+ }
+ }
+}
diff --git a/src/ImageSharp/Memory/Allocators/AllocationOptionsExtensions.cs b/src/ImageSharp/Memory/Allocators/AllocationOptionsExtensions.cs
index 3ead1c5df..986ed7f7c 100644
--- a/src/ImageSharp/Memory/Allocators/AllocationOptionsExtensions.cs
+++ b/src/ImageSharp/Memory/Allocators/AllocationOptionsExtensions.cs
@@ -1,9 +1,19 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Memory;
+///
+/// Provides helper methods for working with .
+///
internal static class AllocationOptionsExtensions
{
- public static bool Has(this AllocationOptions options, AllocationOptions flag) => (options & flag) == flag;
+ ///
+ /// Returns a value indicating whether the specified flag is set on the allocation options.
+ ///
+ /// The allocation options to inspect.
+ /// The flag to test for.
+ /// if is set; otherwise, .
+ public static bool Has(this AllocationOptions options, AllocationOptions flag)
+ => (options & flag) == flag;
}
diff --git a/src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs b/src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs
index 9f34602fb..c22e827a8 100644
--- a/src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs
+++ b/src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs
@@ -47,7 +47,7 @@ internal class BasicArrayBuffer : ManagedBufferBase
public override Span GetSpan() => this.Array.AsSpan(0, this.Length);
///
- protected override void Dispose(bool disposing)
+ protected override void DisposeCore(bool disposing)
{
}
diff --git a/src/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs b/src/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs
index a6ed797d6..84dd065f5 100644
--- a/src/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs
+++ b/src/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs
@@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Memory.Internals;
/// Provides a base class for implementations by implementing pinning logic for adaption.
///
/// The element type.
-internal abstract class ManagedBufferBase : MemoryManager
+internal abstract class ManagedBufferBase : AllocationTrackedMemoryManager
where T : struct
{
private GCHandle pinHandle;
diff --git a/src/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs b/src/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs
index 4a202a96c..b0fe0b649 100644
--- a/src/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs
+++ b/src/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs
@@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Memory.Internals;
///
internal abstract class RefCountedMemoryLifetimeGuard : IDisposable
{
+ private AllocationTrackingState allocationTracking;
private int refCount = 1;
private int disposed;
private int released;
@@ -38,6 +39,14 @@ internal abstract class RefCountedMemoryLifetimeGuard : IDisposable
public void ReleaseRef() => this.ReleaseRef(false);
+ ///
+ /// Attaches allocator reservation tracking to this lifetime guard.
+ ///
+ /// The allocator that owns the reservation.
+ /// The reserved allocation size, in bytes.
+ public void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes)
+ => this.allocationTracking.Attach(allocator, lengthInBytes);
+
public void Dispose()
{
int wasDisposed = Interlocked.Exchange(ref this.disposed, 1);
@@ -69,6 +78,10 @@ internal abstract class RefCountedMemoryLifetimeGuard : IDisposable
}
this.Release();
+
+ // Guard-backed resources can be recovered by finalization, so their allocator
+ // reservation must follow the guard's actual release point instead of the owner object.
+ this.allocationTracking.Release();
}
}
}
diff --git a/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs b/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs
index 02bdf0f48..97fab1b68 100644
--- a/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs
+++ b/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs
@@ -13,7 +13,7 @@ internal class SharedArrayPoolBuffer : ManagedBufferBase, IRefCounted
where T : struct
{
private readonly int lengthInBytes;
- private LifetimeGuard lifetimeGuard;
+ private readonly LifetimeGuard lifetimeGuard;
public SharedArrayPoolBuffer(int lengthInElements)
{
@@ -24,7 +24,10 @@ internal class SharedArrayPoolBuffer : ManagedBufferBase, IRefCounted
public byte[]? Array { get; private set; }
- protected override void Dispose(bool disposing)
+ protected internal override void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes)
+ => this.lifetimeGuard.AttachAllocationTracking(allocator, lengthInBytes);
+
+ protected override void DisposeCore(bool disposing)
{
if (this.Array == null)
{
diff --git a/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs
index 854b40e0c..3a729cdf2 100644
--- a/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs
+++ b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs
@@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Memory.Internals;
/// access to unmanaged buffers allocated by .
///
/// The element type.
-internal sealed unsafe class UnmanagedBuffer : MemoryManager, IRefCounted
+internal sealed unsafe class UnmanagedBuffer : AllocationTrackedMemoryManager, IRefCounted
where T : struct
{
private readonly int lengthInElements;
@@ -31,6 +31,9 @@ internal sealed unsafe class UnmanagedBuffer : MemoryManager, IRefCounted
public void* Pointer => this.lifetimeGuard.Handle.Pointer;
+ protected internal override void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes)
+ => this.lifetimeGuard.AttachAllocationTracking(allocator, lengthInBytes);
+
public override Span GetSpan()
{
DebugGuard.NotDisposed(this.disposed == 1, this.GetType().Name);
@@ -52,7 +55,7 @@ internal sealed unsafe class UnmanagedBuffer : MemoryManager, IRefCounted
}
///
- protected override void Dispose(bool disposing)
+ protected override void DisposeCore(bool disposing)
{
DebugGuard.IsTrue(disposing, nameof(disposing), "Unmanaged buffers should not have finalizer!");
diff --git a/src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs
index 6b31cadf4..632e1bec0 100644
--- a/src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs
+++ b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs
@@ -39,13 +39,13 @@ internal struct UnmanagedMemoryHandle : IEquatable
Interlocked.Increment(ref totalOutstandingHandles);
}
- public IntPtr Handle => this.handle;
+ public readonly IntPtr Handle => this.handle;
- public bool IsInvalid => this.Handle == IntPtr.Zero;
+ public readonly bool IsInvalid => this.Handle == IntPtr.Zero;
- public bool IsValid => this.Handle != IntPtr.Zero;
+ public readonly bool IsValid => this.Handle != IntPtr.Zero;
- public unsafe void* Pointer => (void*)this.Handle;
+ public readonly unsafe void* Pointer => (void*)this.Handle;
///
/// Gets the total outstanding handle allocations for testing purposes.
@@ -121,9 +121,9 @@ internal struct UnmanagedMemoryHandle : IEquatable
this.lengthInBytes = 0;
}
- public bool Equals(UnmanagedMemoryHandle other) => this.handle.Equals(other.handle);
+ public readonly bool Equals(UnmanagedMemoryHandle other) => this.handle.Equals(other.handle);
- public override bool Equals(object? obj) => obj is UnmanagedMemoryHandle other && this.Equals(other);
+ public override readonly bool Equals(object? obj) => obj is UnmanagedMemoryHandle other && this.Equals(other);
- public override int GetHashCode() => this.handle.GetHashCode();
+ public override readonly int GetHashCode() => this.handle.GetHashCode();
}
diff --git a/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
index 8eaf0b6d6..591c3b9dd 100644
--- a/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
+++ b/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
@@ -12,6 +12,10 @@ namespace SixLabors.ImageSharp.Memory;
public abstract class MemoryAllocator
{
private const int OneGigabyte = 1 << 30;
+ private long accumulativeAllocatedBytes;
+ private long memoryGroupAllocationLimitBytes = Environment.Is64BitProcess ? 4L * OneGigabyte : OneGigabyte;
+ private long accumulativeAllocationLimitBytes = long.MaxValue;
+ private int singleBufferAllocationLimitBytes = OneGigabyte;
///
/// Gets the default platform-specific global instance that
@@ -23,9 +27,68 @@ public abstract class MemoryAllocator
///
public static MemoryAllocator Default { get; } = Create();
- internal long MemoryGroupAllocationLimitBytes { get; private set; } = Environment.Is64BitProcess ? 4L * OneGigabyte : OneGigabyte;
+ ///
+ /// Gets or sets the maximum number of bytes that can be allocated by a memory group.
+ /// A memory group backs the pixel buffer of a single image, so this limits the total image size.
+ ///
+ ///
+ /// The default limit is determined by the process architecture: 4 GB for 64-bit processes and
+ /// 1 GB for 32-bit processes. The setter is available to derived allocators and requires a positive value.
+ ///
+ /// The value is not greater than zero.
+ public long MemoryGroupAllocationLimitBytes
+ {
+ get => this.memoryGroupAllocationLimitBytes;
+ protected set
+ {
+ Guard.MustBeGreaterThan(value, 0, nameof(this.MemoryGroupAllocationLimitBytes));
+ this.memoryGroupAllocationLimitBytes = value;
+ }
+ }
- internal int SingleBufferAllocationLimitBytes { get; private set; } = OneGigabyte;
+ ///
+ /// Gets or sets the maximum accumulative size, in bytes, of all active allocations made through this allocator instance.
+ ///
+ ///
+ /// Defaults to , effectively imposing no limit on the accumulative total.
+ /// When set, this provides a safeguard against excessive memory consumption by capping the combined size of
+ /// outstanding allocations issued by this instance.
+ /// When the accumulative size of active allocations exceeds this limit, an will be thrown to
+ /// prevent further allocations and signal that the limit has been breached.
+ /// The setter is available to derived allocators and requires a positive value.
+ ///
+ /// The value is not greater than zero.
+ public long AccumulativeAllocationLimitBytes
+ {
+ get => this.accumulativeAllocationLimitBytes;
+ protected set
+ {
+ Guard.MustBeGreaterThan(value, 0, nameof(this.AccumulativeAllocationLimitBytes));
+ this.accumulativeAllocationLimitBytes = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the maximum size, in bytes, that can be allocated for a single contiguous buffer.
+ /// This limit applies to and to contiguous image buffers
+ /// requested through .
+ ///
+ ///
+ /// The single buffer allocation limit is set to 1 GB by default.
+ /// A single contiguous buffer can never exceed bytes; larger images are
+ /// backed by discontiguous memory groups limited by .
+ /// The setter is available to derived allocators and requires a positive value.
+ ///
+ /// The value is not greater than zero.
+ public int SingleBufferAllocationLimitBytes
+ {
+ get => this.singleBufferAllocationLimitBytes;
+ protected set
+ {
+ Guard.MustBeGreaterThan(value, 0, nameof(this.SingleBufferAllocationLimitBytes));
+ this.singleBufferAllocationLimitBytes = value;
+ }
+ }
///
/// Gets the length of the largest contiguous buffer that can be handled by this allocator instance in bytes.
@@ -47,13 +110,38 @@ public abstract class MemoryAllocator
public static MemoryAllocator Create(MemoryAllocatorOptions options)
{
UniformUnmanagedMemoryPoolMemoryAllocator allocator = new(options.MaximumPoolSizeMegabytes);
+ allocator.ApplyOptions(options);
+ return allocator;
+ }
+
+ ///
+ /// Applies the supplied to this instance.
+ /// Derived allocators can call this from their constructors to accept user configuration.
+ ///
+ /// The options to apply. Properties left as are ignored.
+ ///
+ /// The applied single buffer limit is capped to ,
+ /// because a single contiguous buffer can never be larger than the total allocation limit.
+ ///
+ protected void ApplyOptions(MemoryAllocatorOptions options)
+ {
if (options.AllocationLimitMegabytes.HasValue)
{
- allocator.MemoryGroupAllocationLimitBytes = options.AllocationLimitMegabytes.Value * 1024L * 1024L;
- allocator.SingleBufferAllocationLimitBytes = (int)Math.Min(allocator.SingleBufferAllocationLimitBytes, allocator.MemoryGroupAllocationLimitBytes);
+ this.MemoryGroupAllocationLimitBytes = options.AllocationLimitMegabytes.Value * 1024L * 1024L;
}
- return allocator;
+ if (options.SingleBufferAllocationLimitMegabytes.HasValue)
+ {
+ // The option setter caps the value at 2047 MB, so converting to bytes cannot overflow.
+ this.SingleBufferAllocationLimitBytes = (int)(options.SingleBufferAllocationLimitMegabytes.Value * 1024L * 1024L);
+ }
+
+ this.SingleBufferAllocationLimitBytes = (int)Math.Min(this.SingleBufferAllocationLimitBytes, this.MemoryGroupAllocationLimitBytes);
+
+ if (options.AccumulativeAllocationLimitMegabytes.HasValue)
+ {
+ this.AccumulativeAllocationLimitBytes = options.AccumulativeAllocationLimitMegabytes.Value * 1024L * 1024L;
+ }
}
///
@@ -63,15 +151,60 @@ public abstract class MemoryAllocator
/// Size of the buffer to allocate.
/// The allocation options.
/// A buffer of values of type .
- /// When length is zero or negative.
- /// When length is over the capacity of the allocator.
- public abstract IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None)
+ /// When length is negative or over the capacity of the allocator.
+ public IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None)
+ where T : struct
+ {
+ long lengthInBytes = this.GetValidatedAllocationLengthInBytes(length);
+ bool shouldTrack = this.AccumulativeAllocationLimitBytes != long.MaxValue && lengthInBytes != 0;
+ if (shouldTrack)
+ {
+ this.ReserveAllocation(lengthInBytes);
+ }
+
+ try
+ {
+ AllocationTrackedMemoryManager owner = this.AllocateCore(length, options);
+ if (shouldTrack)
+ {
+ owner.AttachAllocationTracking(this, lengthInBytes);
+ }
+
+ return owner;
+ }
+ catch
+ {
+ if (shouldTrack)
+ {
+ this.ReleaseAccumulatedBytes(lengthInBytes);
+ }
+
+ throw;
+ }
+ }
+
+ ///
+ /// Allocates a tracked memory owner for .
+ ///
+ /// Type of the data stored in the buffer.
+ /// Size of the buffer to allocate.
+ /// The allocation options.
+ /// A tracked memory owner of values of type .
+ ///
+ /// Implementations should only allocate and initialize the concrete owner. The base allocator
+ /// reserves bytes, attaches tracking to the returned owner, and releases the reservation if allocation fails.
+ ///
+ protected abstract AllocationTrackedMemoryManager AllocateCore(int length, AllocationOptions options = AllocationOptions.None)
where T : struct;
///
/// Releases all retained resources not being in use.
/// Eg: by resetting array pools and letting GC to free the arrays.
///
+ ///
+ /// This does not dispose active allocations; callers are responsible for disposing all
+ /// instances to release memory.
+ ///
public virtual void ReleaseRetainedResources()
{
}
@@ -102,11 +235,109 @@ public abstract class MemoryAllocator
InvalidMemoryOperationException.ThrowAllocationOverLimitException(totalLengthInBytes, this.MemoryGroupAllocationLimitBytes);
}
- // Cast to long is safe because we already checked that the total length is within the limit.
- return this.AllocateGroupCore(totalLength, (long)totalLengthInBytes, bufferAlignment, options);
+ long totalLengthInBytesLong = (long)totalLengthInBytes;
+ bool shouldTrack = this.AccumulativeAllocationLimitBytes != long.MaxValue && totalLengthInBytesLong != 0;
+ if (shouldTrack)
+ {
+ this.ReserveAllocation(totalLengthInBytesLong);
+ }
+
+ try
+ {
+ MemoryGroup group = this.AllocateGroupCore(totalLength, totalLengthInBytesLong, bufferAlignment, options);
+ if (shouldTrack)
+ {
+ group.AttachAllocationTracking(this, totalLengthInBytesLong);
+ }
+
+ return group;
+ }
+ catch
+ {
+ if (shouldTrack)
+ {
+ this.ReleaseAccumulatedBytes(totalLengthInBytesLong);
+ }
+
+ throw;
+ }
}
internal virtual MemoryGroup AllocateGroupCore(long totalLengthInElements, long totalLengthInBytes, int bufferAlignment, AllocationOptions options)
where T : struct
=> MemoryGroup.Allocate(this, totalLengthInElements, bufferAlignment, options);
+
+ ///
+ /// Allocates a single segment for construction.
+ ///
+ /// Type of the data stored in the buffer.
+ /// Size of the segment to allocate.
+ /// The allocation options.
+ /// A segment owner for the requested buffer length.
+ ///
+ /// The default implementation validates the segment size then calls
+ /// directly so group construction can reserve and release the total allocation once.
+ ///
+ internal virtual IMemoryOwner AllocateGroupBuffer(int length, AllocationOptions options = AllocationOptions.None)
+ where T : struct
+ {
+ _ = this.GetValidatedAllocationLengthInBytes(length);
+ return this.AllocateCore(length, options);
+ }
+
+ ///
+ /// Returns the validated allocation length in bytes.
+ ///
+ /// Type of the data stored in the buffer.
+ /// Size of the buffer to allocate.
+ /// The allocation length in bytes.
+ private long GetValidatedAllocationLengthInBytes(int length)
+ where T : struct
+ {
+ if (length < 0)
+ {
+ InvalidMemoryOperationException.ThrowNegativeAllocationException(length);
+ }
+
+ ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf();
+ if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes)
+ {
+ InvalidMemoryOperationException.ThrowAllocationOverLimitException(lengthInBytes, this.SingleBufferAllocationLimitBytes);
+ }
+
+ return (long)lengthInBytes;
+ }
+
+ ///
+ /// Reserves accumulative allocation bytes before creating the underlying buffer.
+ ///
+ /// The number of bytes to reserve.
+ private void ReserveAllocation(long lengthInBytes)
+ {
+ if (lengthInBytes <= 0)
+ {
+ return;
+ }
+
+ long total = Interlocked.Add(ref this.accumulativeAllocatedBytes, lengthInBytes);
+ if (total > this.AccumulativeAllocationLimitBytes)
+ {
+ _ = Interlocked.Add(ref this.accumulativeAllocatedBytes, -lengthInBytes);
+ InvalidMemoryOperationException.ThrowAccumulativeAllocationOverLimitException(lengthInBytes, total, this.AccumulativeAllocationLimitBytes);
+ }
+ }
+
+ ///
+ /// Releases accumulative allocation bytes previously tracked by this allocator.
+ ///
+ /// The number of bytes to release.
+ internal void ReleaseAccumulatedBytes(long lengthInBytes)
+ {
+ if (lengthInBytes <= 0)
+ {
+ return;
+ }
+
+ _ = Interlocked.Add(ref this.accumulativeAllocatedBytes, -lengthInBytes);
+ }
}
diff --git a/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs b/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs
index d9ba62c1e..0578fd64d 100644
--- a/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs
+++ b/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs
@@ -8,8 +8,15 @@ namespace SixLabors.ImageSharp.Memory;
///
public struct MemoryAllocatorOptions
{
+ ///
+ /// The largest single-buffer limit, in Megabytes, that still fits bytes.
+ ///
+ private const int MaxSingleBufferAllocationLimitMegabytes = 2047;
+
private int? maximumPoolSizeMegabytes;
private int? allocationLimitMegabytes;
+ private int? accumulativeAllocationLimitMegabytes;
+ private int? singleBufferAllocationLimitMegabytes;
///
/// Gets or sets a value defining the maximum size of the 's internal memory pool
@@ -17,7 +24,7 @@ public struct MemoryAllocatorOptions
///
public int? MaximumPoolSizeMegabytes
{
- get => this.maximumPoolSizeMegabytes;
+ readonly get => this.maximumPoolSizeMegabytes;
set
{
if (value.HasValue)
@@ -35,15 +42,78 @@ public struct MemoryAllocatorOptions
///
public int? AllocationLimitMegabytes
{
- get => this.allocationLimitMegabytes;
+ readonly get => this.allocationLimitMegabytes;
set
{
if (value.HasValue)
{
Guard.MustBeGreaterThan(value.Value, 0, nameof(this.AllocationLimitMegabytes));
+ if (this.AccumulativeAllocationLimitMegabytes.HasValue)
+ {
+ Guard.MustBeLessThanOrEqualTo(
+ value.Value,
+ this.AccumulativeAllocationLimitMegabytes.Value,
+ nameof(this.AllocationLimitMegabytes));
+ }
}
this.allocationLimitMegabytes = value;
}
}
+
+ ///
+ /// Gets or sets a value defining the maximum size, in Megabytes, of a single contiguous buffer
+ /// that the created can allocate.
+ /// means the default of 1 GB.
+ ///
+ ///
+ /// This limit applies to contiguous buffers, including image buffers requested through
+ /// . A single contiguous buffer can never exceed
+ /// bytes, so the largest accepted value is 2047. The applied limit is also
+ /// capped to because a single buffer can never be larger
+ /// than the total allocation limit.
+ ///
+ public int? SingleBufferAllocationLimitMegabytes
+ {
+ readonly get => this.singleBufferAllocationLimitMegabytes;
+ set
+ {
+ if (value.HasValue)
+ {
+ Guard.MustBeGreaterThan(value.Value, 0, nameof(this.SingleBufferAllocationLimitMegabytes));
+ Guard.MustBeLessThanOrEqualTo(
+ value.Value,
+ MaxSingleBufferAllocationLimitMegabytes,
+ nameof(this.SingleBufferAllocationLimitMegabytes));
+ }
+
+ this.singleBufferAllocationLimitMegabytes = value;
+ }
+ }
+
+ ///
+ /// Gets or sets a value defining the maximum accumulative size, in Megabytes, of all active allocations made
+ /// through the created instance.
+ /// (the default) imposes no limit on the accumulative total.
+ ///
+ public int? AccumulativeAllocationLimitMegabytes
+ {
+ readonly get => this.accumulativeAllocationLimitMegabytes;
+ set
+ {
+ if (value.HasValue)
+ {
+ Guard.MustBeGreaterThan(value.Value, 0, nameof(this.AccumulativeAllocationLimitMegabytes));
+ if (this.AllocationLimitMegabytes.HasValue)
+ {
+ Guard.MustBeGreaterThanOrEqualTo(
+ value.Value,
+ this.AllocationLimitMegabytes.Value,
+ nameof(this.AccumulativeAllocationLimitMegabytes));
+ }
+ }
+
+ this.accumulativeAllocationLimitMegabytes = value;
+ }
+ }
}
diff --git a/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs
index 675afe8b9..5d183fa44 100644
--- a/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs
+++ b/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs
@@ -1,8 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.Buffers;
-using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory.Internals;
namespace SixLabors.ImageSharp.Memory;
@@ -12,23 +10,24 @@ namespace SixLabors.ImageSharp.Memory;
///
public sealed class SimpleGcMemoryAllocator : MemoryAllocator
{
+ ///
+ /// Initializes a new instance of the class with default limits.
+ ///
+ public SimpleGcMemoryAllocator()
+ : this(default)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with custom limits.
+ ///
+ /// The to apply.
+ public SimpleGcMemoryAllocator(MemoryAllocatorOptions options) => this.ApplyOptions(options);
+
///
protected internal override int GetBufferCapacityInBytes() => int.MaxValue;
///
- public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None)
- {
- if (length < 0)
- {
- InvalidMemoryOperationException.ThrowNegativeAllocationException(length);
- }
-
- ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf();
- if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes)
- {
- InvalidMemoryOperationException.ThrowAllocationOverLimitException(lengthInBytes, this.SingleBufferAllocationLimitBytes);
- }
-
- return new BasicArrayBuffer(new T[length]);
- }
+ protected override AllocationTrackedMemoryManager AllocateCore(int length, AllocationOptions options = AllocationOptions.None)
+ => new BasicArrayBuffer(new T[length]);
}
diff --git a/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs
index 10defe6cd..cfffc679a 100644
--- a/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs
+++ b/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs
@@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory.Internals;
@@ -71,30 +70,25 @@ internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocato
this.nonPoolAllocator = new UnmanagedMemoryAllocator(unmanagedBufferSizeInBytes);
}
- // This delegate allows overriding the method returning the available system memory,
- // so we can test our workaround for https://github.com/dotnet/runtime/issues/65466
- internal static Func GetTotalAvailableMemoryBytes { get; set; } = () => GC.GetGCMemoryInfo().TotalAvailableMemoryBytes;
+ internal UniformUnmanagedMemoryPoolMemoryAllocator(
+ int sharedArrayPoolThresholdInBytes,
+ int poolBufferSizeInBytes,
+ long maxPoolSizeInBytes,
+ int unmanagedBufferSizeInBytes,
+ MemoryAllocatorOptions options)
+ : this(sharedArrayPoolThresholdInBytes, poolBufferSizeInBytes, maxPoolSizeInBytes, unmanagedBufferSizeInBytes)
+ => this.ApplyOptions(options);
///
protected internal override int GetBufferCapacityInBytes() => this.poolBufferSizeInBytes;
///
- public override IMemoryOwner Allocate(
+ protected override AllocationTrackedMemoryManager AllocateCore(
int length,
AllocationOptions options = AllocationOptions.None)
{
- if (length < 0)
- {
- InvalidMemoryOperationException.ThrowNegativeAllocationException(length);
- }
-
- ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf();
- if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes)
- {
- InvalidMemoryOperationException.ThrowAllocationOverLimitException(lengthInBytes, this.SingleBufferAllocationLimitBytes);
- }
-
- if (lengthInBytes <= (ulong)this.sharedArrayPoolThresholdInBytes)
+ int lengthInBytes = length * Unsafe.SizeOf();
+ if (lengthInBytes <= this.sharedArrayPoolThresholdInBytes)
{
SharedArrayPoolBuffer buffer = new(length);
if (options.Has(AllocationOptions.Clean))
@@ -105,17 +99,16 @@ internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocato
return buffer;
}
- if (lengthInBytes <= (ulong)this.poolBufferSizeInBytes)
+ if (lengthInBytes <= this.poolBufferSizeInBytes)
{
UnmanagedMemoryHandle mem = this.pool.Rent();
if (mem.IsValid)
{
- UnmanagedBuffer buffer = this.pool.CreateGuardedBuffer(mem, length, options.Has(AllocationOptions.Clean));
- return buffer;
+ return this.pool.CreateGuardedBuffer(mem, length, options.Has(AllocationOptions.Clean));
}
}
- return this.nonPoolAllocator.Allocate(length, options);
+ return UnmanagedMemoryAllocator.AllocateBuffer(length, options);
}
///
@@ -155,20 +148,14 @@ internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocato
private static long GetDefaultMaxPoolSizeBytes()
{
- // On 64 bit set the pool size to a portion of the total available memory.
- // https://github.com/dotnet/runtime/issues/55126#issuecomment-876779327
if (Environment.Is64BitProcess)
{
- long total = GetTotalAvailableMemoryBytes();
-
- // Workaround for https://github.com/dotnet/runtime/issues/65466
- if (total > 0)
- {
- return (long)((ulong)total / 8);
- }
+ // On 64 bit set the pool size to a portion of the total available memory.
+ GCMemoryInfo info = GC.GetGCMemoryInfo();
+ return info.TotalAvailableMemoryBytes / 8;
}
- // Stick to a conservative value of 128 Megabytes on other platforms and 32 bit .NET 5.0:
+ // Stick to a conservative value of 128 Megabytes on 32 bit.
return 128 * OneMegabyte;
}
}
diff --git a/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs
index daf1a7992..eb52da7c0 100644
--- a/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs
+++ b/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs
@@ -18,7 +18,14 @@ internal class UnmanagedMemoryAllocator : MemoryAllocator
protected internal override int GetBufferCapacityInBytes() => this.bufferCapacityInBytes;
- public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None)
+ protected override AllocationTrackedMemoryManager AllocateCore(int length, AllocationOptions options = AllocationOptions.None)
+ where T : struct
+ => AllocateBuffer(length, options);
+
+ // The pooled allocator uses this internal entry point when it needs a raw unmanaged owner without
+ // nesting another allocator-level reservation cycle around the fallback allocation.
+ internal static UnmanagedBuffer AllocateBuffer(int length, AllocationOptions options = AllocationOptions.None)
+ where T : struct
{
UnmanagedBuffer buffer = UnmanagedBuffer.Allocate(length);
if (options.Has(AllocationOptions.Clean))
diff --git a/src/ImageSharp/Memory/Buffer2DExtensions.cs b/src/ImageSharp/Memory/Buffer2DExtensions.cs
index f0fa1438d..290d978d0 100644
--- a/src/ImageSharp/Memory/Buffer2DExtensions.cs
+++ b/src/ImageSharp/Memory/Buffer2DExtensions.cs
@@ -45,7 +45,7 @@ public static class Buffer2DExtensions
Buffer2DRegion sourceRegion = source.GetRegion(rectangle);
if (sourceRegion.IsFullBufferArea)
{
- sourceRegion.Buffer.FastMemoryGroup.CopyTo(buffer.FastMemoryGroup);
+ sourceRegion.Buffer.CopyTo(buffer);
}
else
{
@@ -81,7 +81,7 @@ public static class Buffer2DExtensions
CheckColumnRegionsDoNotOverlap(buffer, sourceIndex, destinationIndex, columnCount);
int elementSize = Unsafe.SizeOf();
- int width = buffer.Width * elementSize;
+ int rowByteStride = buffer.RowStride * elementSize;
int sOffset = sourceIndex * elementSize;
int dOffset = destinationIndex * elementSize;
long count = columnCount * elementSize;
@@ -98,66 +98,46 @@ public static class Buffer2DExtensions
Buffer.MemoryCopy(sPtr, dPtr, count, count);
- basePtr += width;
+ basePtr += rowByteStride;
}
}
}
///
- /// Returns a representing the full area of the buffer.
- ///
- /// The element type
- /// The
- /// The
- internal static Rectangle FullRectangle(this Buffer2D buffer)
- where T : struct
- => new(0, 0, buffer.Width, buffer.Height);
-
- ///
- /// Return a to the subregion represented by 'rectangle'
+ /// Return a to the subregion represented by .
///
/// The element type
/// The
/// The rectangle subregion
/// The
- internal static Buffer2DRegion GetRegion(this Buffer2D buffer, Rectangle rectangle)
+ public static Buffer2DRegion GetRegion(this Buffer2D buffer, Rectangle rectangle)
where T : unmanaged =>
new(buffer, rectangle);
- internal static Buffer2DRegion GetRegion(this Buffer2D buffer, int x, int y, int width, int height)
+ ///
+ /// Return a to the specified area of .
+ ///
+ /// The element type.
+ /// The .
+ /// The X coordinate of the region.
+ /// The Y coordinate of the region.
+ /// The region width.
+ /// The region height.
+ /// The .
+ public static Buffer2DRegion GetRegion(this Buffer2D buffer, int x, int y, int width, int height)
where T : unmanaged =>
new(buffer, new Rectangle(x, y, width, height));
///
- /// Return a to the whole area of 'buffer'
+ /// Return a to the whole area of .
///
/// The element type
/// The
/// The
- internal static Buffer2DRegion GetRegion(this Buffer2D buffer)
+ public static Buffer2DRegion GetRegion(this Buffer2D buffer)
where T : unmanaged =>
new(buffer);
- ///
- /// Returns the size of the buffer.
- ///
- /// The element type
- /// The
- /// The of the buffer
- internal static Size Size(this Buffer2D buffer)
- where T : struct =>
- new(buffer.Width, buffer.Height);
-
- ///
- /// Gets the bounds of the buffer.
- ///
- /// The element type
- /// The
- /// The
- internal static Rectangle Bounds(this Buffer2D buffer)
- where T : struct =>
- new(0, 0, buffer.Width, buffer.Height);
-
[Conditional("DEBUG")]
private static void CheckColumnRegionsDoNotOverlap(
Buffer2D buffer,
diff --git a/src/ImageSharp/Memory/Buffer2DRegion{T}.cs b/src/ImageSharp/Memory/Buffer2DRegion{T}.cs
index f4b257b58..c78c4ce9e 100644
--- a/src/ImageSharp/Memory/Buffer2DRegion{T}.cs
+++ b/src/ImageSharp/Memory/Buffer2DRegion{T}.cs
@@ -15,17 +15,17 @@ public readonly struct Buffer2DRegion
/// Initializes a new instance of the struct.
///
/// The .
- /// The defining a rectangular area within the buffer.
+ /// The defining a rectangular area within the buffer.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public Buffer2DRegion(Buffer2D buffer, Rectangle rectangle)
+ public Buffer2DRegion(Buffer2D buffer, Rectangle bounds)
{
- DebugGuard.MustBeGreaterThanOrEqualTo(rectangle.X, 0, nameof(rectangle));
- DebugGuard.MustBeGreaterThanOrEqualTo(rectangle.Y, 0, nameof(rectangle));
- DebugGuard.MustBeLessThanOrEqualTo(rectangle.Width, buffer.Width, nameof(rectangle));
- DebugGuard.MustBeLessThanOrEqualTo(rectangle.Height, buffer.Height, nameof(rectangle));
+ DebugGuard.MustBeGreaterThanOrEqualTo(bounds.X, 0, nameof(bounds));
+ DebugGuard.MustBeGreaterThanOrEqualTo(bounds.Y, 0, nameof(bounds));
+ DebugGuard.MustBeLessThanOrEqualTo(bounds.Width, buffer.Width, nameof(bounds));
+ DebugGuard.MustBeLessThanOrEqualTo(bounds.Height, buffer.Height, nameof(bounds));
this.Buffer = buffer;
- this.Rectangle = rectangle;
+ this.Bounds = bounds;
}
///
@@ -34,15 +34,10 @@ public readonly struct Buffer2DRegion
/// The .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Buffer2DRegion(Buffer2D buffer)
- : this(buffer, buffer.FullRectangle())
+ : this(buffer, buffer.Bounds)
{
}
- ///
- /// Gets the rectangle specifying the boundaries of the area in .
- ///
- public Rectangle Rectangle { get; }
-
///
/// Gets the being pointed by this instance.
///
@@ -51,27 +46,32 @@ public readonly struct Buffer2DRegion
///
/// Gets the width
///
- public int Width => this.Rectangle.Width;
+ public int Width => this.Bounds.Width;
///
/// Gets the height
///
- public int Height => this.Rectangle.Height;
+ public int Height => this.Bounds.Height;
///
- /// Gets the pixel stride which is equal to the width of .
+ /// Gets the number of elements between row starts in .
///
- public int Stride => this.Buffer.Width;
+ public int Stride => this.Buffer.RowStride;
///
/// Gets the size of the area.
///
- internal Size Size => this.Rectangle.Size;
+ public Size Size => this.Bounds.Size;
+
+ ///
+ /// Gets the rectangle specifying the boundaries of the area in .
+ ///
+ public Rectangle Bounds { get; }
///
/// Gets a value indicating whether the area refers to the entire
///
- internal bool IsFullBufferArea => this.Size == this.Buffer.Size();
+ internal bool IsFullBufferArea => this.Size == this.Buffer.Size;
///
/// Gets or sets a value at the given index.
@@ -79,7 +79,7 @@ public readonly struct Buffer2DRegion
/// The position inside a row
/// The row index
/// The reference to the value
- internal ref T this[int x, int y] => ref this.Buffer[x + this.Rectangle.X, y + this.Rectangle.Y];
+ internal ref T this[int x, int y] => ref this.Buffer[x + this.Bounds.X, y + this.Bounds.Y];
///
/// Gets a span to row 'y' inside this area.
@@ -89,9 +89,9 @@ public readonly struct Buffer2DRegion
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span DangerousGetRowSpan(int y)
{
- int yy = this.Rectangle.Y + y;
- int xx = this.Rectangle.X;
- int width = this.Rectangle.Width;
+ int yy = this.Bounds.Y + y;
+ int xx = this.Bounds.X;
+ int width = this.Bounds.Width;
return this.Buffer.DangerousGetRowSpan(yy).Slice(xx, width);
}
@@ -114,16 +114,16 @@ public readonly struct Buffer2DRegion
///
/// Returns a subregion as . (Similar to .)
///
- /// The specifying the boundaries of the subregion
+ /// The specifying the boundaries of the subregion
/// The subregion
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Buffer2DRegion GetSubRegion(Rectangle rectangle)
{
- DebugGuard.MustBeLessThanOrEqualTo(rectangle.Width, this.Rectangle.Width, nameof(rectangle));
- DebugGuard.MustBeLessThanOrEqualTo(rectangle.Height, this.Rectangle.Height, nameof(rectangle));
+ DebugGuard.MustBeLessThanOrEqualTo(rectangle.Width, this.Bounds.Width, nameof(rectangle));
+ DebugGuard.MustBeLessThanOrEqualTo(rectangle.Height, this.Bounds.Height, nameof(rectangle));
- int x = this.Rectangle.X + rectangle.X;
- int y = this.Rectangle.Y + rectangle.Y;
+ int x = this.Bounds.X + rectangle.X;
+ int y = this.Bounds.Y + rectangle.Y;
rectangle = new Rectangle(x, y, rectangle.Width, rectangle.Height);
return new Buffer2DRegion(this.Buffer, rectangle);
}
@@ -135,8 +135,8 @@ public readonly struct Buffer2DRegion
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ref T GetReferenceToOrigin()
{
- int y = this.Rectangle.Y;
- int x = this.Rectangle.X;
+ int y = this.Bounds.Y;
+ int x = this.Bounds.X;
return ref this.Buffer.DangerousGetRowSpan(y)[x];
}
@@ -146,13 +146,13 @@ public readonly struct Buffer2DRegion
internal void Clear()
{
// Optimization for when the size of the area is the same as the buffer size.
- if (this.IsFullBufferArea)
+ if (this.IsFullBufferArea && this.Buffer.RowStride == this.Buffer.Width)
{
- this.Buffer.FastMemoryGroup.Clear();
+ this.Buffer.Clear(default);
return;
}
- for (int y = 0; y < this.Rectangle.Height; y++)
+ for (int y = 0; y < this.Bounds.Height; y++)
{
Span row = this.DangerousGetRowSpan(y);
row.Clear();
@@ -166,13 +166,13 @@ public readonly struct Buffer2DRegion
internal void Fill(T value)
{
// Optimization for when the size of the area is the same as the buffer size.
- if (this.IsFullBufferArea)
+ if (this.IsFullBufferArea && this.Buffer.RowStride == this.Buffer.Width)
{
- this.Buffer.FastMemoryGroup.Fill(value);
+ this.Buffer.Clear(value);
return;
}
- for (int y = 0; y < this.Rectangle.Height; y++)
+ for (int y = 0; y < this.Bounds.Height; y++)
{
Span row = this.DangerousGetRowSpan(y);
row.Fill(value);
diff --git a/src/ImageSharp/Memory/Buffer2D{T}.cs b/src/ImageSharp/Memory/Buffer2D{T}.cs
index 39c6e62e1..cfd7664f0 100644
--- a/src/ImageSharp/Memory/Buffer2D{T}.cs
+++ b/src/ImageSharp/Memory/Buffer2D{T}.cs
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Buffers;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Memory;
@@ -20,21 +21,53 @@ public sealed class Buffer2D : IDisposable
/// The number of elements in a row.
/// The number of rows.
internal Buffer2D(MemoryGroup memoryGroup, int width, int height)
+ : this(memoryGroup, width, height, width)
{
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The to wrap.
+ /// The number of elements in a row.
+ /// The number of rows.
+ /// The number of elements between row starts.
+ internal Buffer2D(MemoryGroup memoryGroup, int width, int height, int rowStride)
+ {
+ Guard.MustBeGreaterThan(width, 0, nameof(width));
+ Guard.MustBeGreaterThan(height, 0, nameof(height));
+ Guard.MustBeGreaterThanOrEqualTo(rowStride, width, nameof(rowStride));
+
this.FastMemoryGroup = memoryGroup;
- this.Width = width;
- this.Height = height;
+ this.Size = new Size(width, height);
+ this.RowStride = rowStride;
}
///
/// Gets the width.
///
- public int Width { get; private set; }
+ public int Width => this.Size.Width;
///
/// Gets the height.
///
- public int Height { get; private set; }
+ public int Height => this.Size.Height;
+
+ ///
+ /// Gets the size of the buffer.
+ ///
+ public Size Size { get; private set; }
+
+ ///
+ /// Gets the bounds of the buffer.
+ ///
+ /// The
+ public Rectangle Bounds => new(0, 0, this.Width, this.Height);
+
+ ///
+ /// Gets the number of elements between row starts in the backing memory.
+ ///
+ public int RowStride { get; private set; }
///
/// Gets the backing .
@@ -75,6 +108,168 @@ public sealed class Buffer2D : IDisposable
}
}
+ ///
+ /// Wraps an existing memory area as a with tightly packed rows.
+ ///
+ ///
+ /// This method does not transfer ownership of to the returned .
+ /// The caller is responsible for ensuring that the memory remains valid for the entire lifetime of the returned buffer.
+ /// If originates from an (for example from ),
+ /// do not dispose that owner while the returned buffer is still in use.
+ ///
+ /// The source memory.
+ /// The number of elements in each row.
+ /// The number of rows.
+ /// The wrapped instance.
+ /// Thrown when or is not positive.
+ /// Thrown when is shorter than width * height.
+#pragma warning disable CA1000 // Do not declare static members on generic types
+ public static Buffer2D WrapMemory(Memory memory, int width, int height)
+#pragma warning restore CA1000 // Do not declare static members on generic types
+ => WrapMemory(memory, width, height, width);
+
+ ///
+ /// Wraps an existing memory area as a using the specified row stride.
+ ///
+ ///
+ /// This method does not transfer ownership of to the returned .
+ /// The caller is responsible for ensuring that the memory remains valid for the entire lifetime of the returned buffer.
+ /// If originates from an (for example from ),
+ /// do not dispose that owner while the returned buffer is still in use.
+ /// The minimum required length is ((height - 1) * stride) + width elements.
+ ///
+ /// The source memory.
+ /// The number of elements in each row.
+ /// The number of rows.
+ /// The number of elements between row starts in the source memory.
+ /// The wrapped instance.
+ ///
+ /// Thrown when or is not positive,
+ /// or when is less than .
+ ///
+ /// Thrown when is shorter than the required buffer size.
+#pragma warning disable CA1000 // Do not declare static members on generic types
+ public static Buffer2D WrapMemory(Memory memory, int width, int height, int stride)
+#pragma warning restore CA1000 // Do not declare static members on generic types
+ {
+ Guard.MustBeGreaterThan(width, 0, nameof(width));
+ Guard.MustBeGreaterThan(height, 0, nameof(height));
+ Guard.MustBeGreaterThanOrEqualTo(stride, width, nameof(stride));
+
+ long requiredLength = checked(((long)(height - 1) * stride) + width);
+ Guard.IsTrue(memory.Length >= requiredLength, nameof(memory), "The length of the input memory is less than the specified buffer size");
+
+ MemoryGroup memorySource = MemoryGroup.Wrap(memory);
+ return new Buffer2D(memorySource, width, height, stride);
+ }
+
+ ///
+ /// Gets the representation of the values as a single contiguous
+ /// when the backing group is a single tightly packed segment.
+ ///
+ /// The referencing the buffer.
+ ///
+ /// when the buffer can be copied as one contiguous block
+ /// without per-row handling; otherwise .
+ ///
+ public bool DangerousTryGetSingleMemory(out Memory memory)
+ {
+ if (this.MemoryGroup.Count > 1 || this.RowStride != this.Width)
+ {
+ memory = default;
+ return false;
+ }
+
+ int logicalLength = checked((int)((long)this.Width * this.Height));
+ memory = this.MemoryGroup[0][..logicalLength];
+ return true;
+ }
+
+ ///
+ /// Copies this buffer into using the source logical row layout.
+ ///
+ ///
+ /// When dimensions are equal, destination stride is respected.
+ /// When dimensions differ, source stride is used to copy the source logical layout into destination memory.
+ ///
+ /// The destination buffer.
+ internal void CopyTo(Buffer2D destination)
+ {
+ Guard.NotNull(destination, nameof(destination));
+
+ bool sameDimensions = this.Width == destination.Width && this.Height == destination.Height;
+ int destinationStride = sameDimensions ? destination.RowStride : this.RowStride;
+
+ // Different dimensions use source logical layout. This supports SwapOrCopyContent,
+ // where metadata is swapped after data copy.
+ this.FastMemoryGroup.CopyTo(
+ this.RowStride,
+ destination.FastMemoryGroup,
+ destinationStride,
+ this.Width,
+ this.Height);
+ }
+
+ ///
+ /// Copies this buffer into using the source row stride as destination layout.
+ ///
+ /// The destination span.
+ internal void CopyTo(Span destination)
+ {
+ long requiredLength = checked(((long)(this.Height - 1) * this.RowStride) + this.Width);
+ Guard.MustBeGreaterThanOrEqualTo(destination.Length, requiredLength, nameof(destination));
+
+ this.FastMemoryGroup.CopyTo(
+ this.RowStride,
+ destination,
+ this.RowStride,
+ this.Width,
+ this.Height);
+ }
+
+ ///
+ /// Copies tightly packed row-major data from into this buffer.
+ ///
+ /// The source data.
+ internal void CopyFrom(ReadOnlySpan source) => this.CopyFrom(source, this.Width);
+
+ ///
+ /// Copies row-major data from into this buffer using
+ /// elements between source row starts.
+ ///
+ /// The source data.
+ /// The number of elements between source row starts.
+ internal void CopyFrom(ReadOnlySpan source, int sourceStride)
+ {
+ Guard.MustBeGreaterThanOrEqualTo(sourceStride, this.Width, nameof(sourceStride));
+
+ long requiredLength = checked(((long)(this.Height - 1) * sourceStride) + this.Width);
+ Guard.MustBeGreaterThanOrEqualTo(source.Length, requiredLength, nameof(source));
+
+ // Copy row by row so padded source rows map correctly into the destination logical rows.
+ int sourceOffset = 0;
+ for (int y = 0; y < this.Height; y++)
+ {
+ source.Slice(sourceOffset, this.Width).CopyTo(this.DangerousGetRowSpan(y));
+ sourceOffset += sourceStride;
+ }
+ }
+
+ ///
+ /// Clears this buffer when is default; otherwise fills it with .
+ ///
+ /// The fill value.
+ internal void Clear(T value)
+ {
+ if (value.Equals(default))
+ {
+ this.FastMemoryGroup.Clear();
+ return;
+ }
+
+ this.FastMemoryGroup.Fill(value);
+ }
+
///
/// Disposes the instance
///
@@ -102,7 +297,13 @@ public sealed class Buffer2D : IDisposable
this.ThrowYOutOfRangeException(y);
}
- return this.FastMemoryGroup.GetRowSpanCoreUnsafe(y, this.Width);
+ if (this.RowStride == this.Width)
+ {
+ return this.FastMemoryGroup.GetRowSpanCoreUnsafe(y, this.Width);
+ }
+
+ int rowStart = checked(y * this.RowStride);
+ return this.FastMemoryGroup[0].Span.Slice(rowStart, this.Width);
}
internal bool DangerousTryGetPaddedRowSpan(int y, int padding, out Span paddedSpan)
@@ -111,8 +312,10 @@ public sealed class Buffer2D : IDisposable
DebugGuard.MustBeLessThan(y, this.Height, nameof(y));
int stride = this.Width + padding;
-
- Span slice = this.FastMemoryGroup.GetRemainingSliceOfBuffer(y * (long)this.Width);
+ long rowStart = y * (long)this.RowStride;
+ Span slice = this.RowStride == this.Width
+ ? this.FastMemoryGroup.GetRemainingSliceOfBuffer(rowStart)
+ : this.FastMemoryGroup[0].Span[checked((int)rowStart)..];
if (slice.Length < stride)
{
@@ -127,7 +330,10 @@ public sealed class Buffer2D : IDisposable
[MethodImpl(InliningOptions.ShortMethod)]
internal ref T GetElementUnsafe(int x, int y)
{
- Span span = this.FastMemoryGroup.GetRowSpanCoreUnsafe(y, this.Width);
+ Span span = this.RowStride == this.Width
+ ? this.FastMemoryGroup.GetRowSpanCoreUnsafe(y, this.Width)
+ : this.FastMemoryGroup[0].Span.Slice(checked(y * this.RowStride), this.Width);
+
return ref span[x];
}
@@ -141,6 +347,13 @@ public sealed class Buffer2D : IDisposable
{
DebugGuard.MustBeGreaterThanOrEqualTo(y, 0, nameof(y));
DebugGuard.MustBeLessThan(y, this.Height, nameof(y));
+
+ if (this.RowStride != this.Width)
+ {
+ int rowStart = checked(y * this.RowStride);
+ return this.FastMemoryGroup[0].Slice(rowStart, this.Width);
+ }
+
return this.FastMemoryGroup.View.GetBoundedMemorySlice(y * (long)this.Width, this.Width);
}
@@ -185,21 +398,30 @@ public sealed class Buffer2D : IDisposable
}
else
{
- if (destination.FastMemoryGroup.TotalLength != source.FastMemoryGroup.TotalLength)
+ long sourceLayoutLength = GetRequiredLength(source.Width, source.Height, source.RowStride);
+ long destinationLayoutLength = GetRequiredLength(destination.Width, destination.Height, destination.RowStride);
+
+ bool destinationCanRepresentSource = destination.FastMemoryGroup.TotalLength >= sourceLayoutLength;
+ bool sourceCanRepresentDestination = source.FastMemoryGroup.TotalLength >= destinationLayoutLength;
+ if (!destinationCanRepresentSource || !sourceCanRepresentDestination)
{
throw new InvalidMemoryOperationException(
"Trying to copy/swap incompatible buffers. This is most likely caused by applying an unsupported processor to wrapped-memory images.");
}
- source.FastMemoryGroup.CopyTo(destination.MemoryGroup);
+ source.CopyTo(destination);
}
- (destination.Width, source.Width) = (source.Width, destination.Width);
- (destination.Height, source.Height) = (source.Height, destination.Height);
+ (destination.Size, source.Size) = (source.Size, destination.Size);
+ (destination.RowStride, source.RowStride) = (source.RowStride, destination.RowStride);
return swapped;
}
[MethodImpl(InliningOptions.ColdPath)]
private void ThrowYOutOfRangeException(int y)
=> throw new ArgumentOutOfRangeException($"DangerousGetRowSpan({y}). Y was out of range. Height={this.Height}");
+
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static long GetRequiredLength(int width, int height, int stride)
+ => checked(((long)(height - 1) * stride) + width);
}
diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs
index 7e9719ea7..03f26aab0 100644
--- a/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs
+++ b/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs
@@ -15,12 +15,12 @@ public interface IMemoryGroup : IReadOnlyList>
/// Gets the number of elements per contiguous sub-buffer preceding the last buffer.
/// The last buffer is allowed to be smaller.
///
- int BufferLength { get; }
+ public int BufferLength { get; }
///
/// Gets the aggregate number of elements in the group.
///
- long TotalLength { get; }
+ public long TotalLength { get; }
///
/// Gets a value indicating whether the group has been invalidated.
@@ -29,7 +29,7 @@ public interface IMemoryGroup : IReadOnlyList>
/// Invalidation usually occurs when an image processor capable to alter the image dimensions replaces
/// the image buffers internally.
///
- bool IsValid { get; }
+ public bool IsValid { get; }
///
/// Returns a value-type implementing an allocation-free enumerator of the memory groups in the current
@@ -39,5 +39,5 @@ public interface IMemoryGroup : IReadOnlyList>
/// implementation, which is still available when casting to one of the underlying interfaces.
///
/// A new instance mapping the current values in use.
- new MemoryGroupEnumerator GetEnumerator();
+ public new MemoryGroupEnumerator GetEnumerator();
}
diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs
index 148b5f6bf..b399d3d70 100644
--- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs
+++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs
@@ -71,128 +71,159 @@ internal static class MemoryGroupExtensions
return memory.Slice(bufferStart, length);
}
- internal static void CopyTo(this IMemoryGroup source, Span target)
+ ///
+ /// Copies a 2D logical region from into
+ /// using the provided source and target strides.
+ ///
+ /// The element type.
+ /// The source memory group.
+ /// Elements between source row starts.
+ /// The destination span.
+ /// Elements between destination row starts.
+ /// The logical row width to copy.
+ /// The number of rows to copy.
+ internal static void CopyTo(
+ this IMemoryGroup source,
+ int sourceStride,
+ Span target,
+ int targetStride,
+ int width,
+ int height)
where T : struct
{
Guard.NotNull(source, nameof(source));
- Guard.MustBeGreaterThanOrEqualTo(target.Length, source.TotalLength, nameof(target));
+ Guard.MustBeGreaterThanOrEqualTo(width, 0, nameof(width));
+ Guard.MustBeGreaterThanOrEqualTo(height, 0, nameof(height));
+ Guard.MustBeGreaterThanOrEqualTo(sourceStride, width, nameof(sourceStride));
+ Guard.MustBeGreaterThanOrEqualTo(targetStride, width, nameof(targetStride));
- MemoryGroupCursor cur = new(source);
- long position = 0;
- while (position < source.TotalLength)
- {
- int fwd = Math.Min(cur.LookAhead(), target.Length);
- cur.GetSpan(fwd).CopyTo(target);
+ long sourceRequired = height == 0 ? 0 : checked(((long)(height - 1) * sourceStride) + width);
+ long targetRequired = height == 0 ? 0 : checked(((long)(height - 1) * targetStride) + width);
+ Guard.MustBeGreaterThanOrEqualTo(source.TotalLength, sourceRequired, nameof(source));
+ Guard.MustBeGreaterThanOrEqualTo(target.Length, targetRequired, nameof(target));
- cur.Forward(fwd);
- target = target[fwd..];
- position += fwd;
+ if (width == 0 || height == 0)
+ {
+ return;
}
- }
- internal static void CopyTo(this Span source, IMemoryGroup target)
- where T : struct
- => CopyTo((ReadOnlySpan)source, target);
+ MemoryGroupCursor sourceCursor = new(source);
+ int sourceSkip = sourceStride - width;
- internal static void CopyTo(this ReadOnlySpan source, IMemoryGroup target)
- where T : struct
- {
- Guard.NotNull(target, nameof(target));
- Guard.MustBeGreaterThanOrEqualTo(target.TotalLength, source.Length, nameof(target));
-
- MemoryGroupCursor cur = new(target);
-
- while (!source.IsEmpty)
+ for (int y = 0; y < height; y++)
{
- int fwd = Math.Min(cur.LookAhead(), source.Length);
- source[..fwd].CopyTo(cur.GetSpan(fwd));
- cur.Forward(fwd);
- source = source[fwd..];
+ int rowStart = checked(y * targetStride);
+ Span destinationRow = target.Slice(rowStart, width);
+ CopyFromCursorToSpan(ref sourceCursor, destinationRow);
+
+ // Trailing padding after the last row is optional, so only skip between rows.
+ if (y < height - 1)
+ {
+ ForwardCursor(ref sourceCursor, sourceSkip);
+ }
}
}
- internal static void CopyTo(this IMemoryGroup? source, IMemoryGroup? target)
+ ///
+ /// Copies a 2D logical region from into
+ /// using the provided source and target strides.
+ ///
+ /// The element type.
+ /// The source memory group.
+ /// Elements between source row starts.
+ /// The destination memory group.
+ /// Elements between destination row starts.
+ /// The logical row width to copy.
+ /// The number of rows to copy.
+ internal static void CopyTo(
+ this IMemoryGroup source,
+ int sourceStride,
+ IMemoryGroup target,
+ int targetStride,
+ int width,
+ int height)
where T : struct
{
Guard.NotNull(source, nameof(source));
Guard.NotNull(target, nameof(target));
Guard.IsTrue(source.IsValid, nameof(source), "Source group must be valid.");
Guard.IsTrue(target.IsValid, nameof(target), "Target group must be valid.");
- Guard.MustBeLessThanOrEqualTo(source.TotalLength, target.TotalLength, "Destination buffer too short!");
+ Guard.MustBeGreaterThanOrEqualTo(width, 0, nameof(width));
+ Guard.MustBeGreaterThanOrEqualTo(height, 0, nameof(height));
+ Guard.MustBeGreaterThanOrEqualTo(sourceStride, width, nameof(sourceStride));
+ Guard.MustBeGreaterThanOrEqualTo(targetStride, width, nameof(targetStride));
- if (source.IsEmpty())
+ long sourceRequired = height == 0 ? 0 : checked(((long)(height - 1) * sourceStride) + width);
+ long targetRequired = height == 0 ? 0 : checked(((long)(height - 1) * targetStride) + width);
+ Guard.MustBeGreaterThanOrEqualTo(source.TotalLength, sourceRequired, nameof(source));
+ Guard.MustBeGreaterThanOrEqualTo(target.TotalLength, targetRequired, nameof(target));
+
+ if (width == 0 || height == 0)
{
return;
}
- long position = 0;
- MemoryGroupCursor srcCur = new(source);
- MemoryGroupCursor trgCur = new(target);
+ MemoryGroupCursor sourceCursor = new(source);
+ MemoryGroupCursor targetCursor = new(target);
+ int sourceSkip = sourceStride - width;
+ int targetSkip = targetStride - width;
- while (position < source.TotalLength)
+ for (int y = 0; y < height; y++)
{
- int fwd = Math.Min(srcCur.LookAhead(), trgCur.LookAhead());
- Span srcSpan = srcCur.GetSpan(fwd);
- Span trgSpan = trgCur.GetSpan(fwd);
- srcSpan.CopyTo(trgSpan);
-
- srcCur.Forward(fwd);
- trgCur.Forward(fwd);
- position += fwd;
+ CopyFromCursorToCursor(ref sourceCursor, ref targetCursor, width);
+
+ // Trailing padding after the last row is optional, so only skip between rows.
+ if (y < height - 1)
+ {
+ ForwardCursor(ref sourceCursor, sourceSkip);
+ ForwardCursor(ref targetCursor, targetSkip);
+ }
}
}
- internal static void TransformTo(
- this IMemoryGroup source,
- IMemoryGroup target,
- TransformItemsDelegate transform)
- where TSource : struct
- where TTarget : struct
+ private static void CopyFromCursorToCursor(
+ ref MemoryGroupCursor source,
+ ref MemoryGroupCursor target,
+ int count)
+ where T : struct
{
- Guard.NotNull(source, nameof(source));
- Guard.NotNull(target, nameof(target));
- Guard.NotNull(transform, nameof(transform));
- Guard.IsTrue(source.IsValid, nameof(source), "Source group must be valid.");
- Guard.IsTrue(target.IsValid, nameof(target), "Target group must be valid.");
- Guard.MustBeLessThanOrEqualTo(source.TotalLength, target.TotalLength, "Destination buffer too short!");
-
- if (source.IsEmpty())
+ int remaining = count;
+ while (remaining > 0)
{
- return;
+ int fwd = Math.Min(remaining, Math.Min(source.LookAhead(), target.LookAhead()));
+ source.GetSpan(fwd).CopyTo(target.GetSpan(fwd));
+ source.Forward(fwd);
+ target.Forward(fwd);
+ remaining -= fwd;
}
+ }
- long position = 0;
- MemoryGroupCursor srcCur = new(source);
- MemoryGroupCursor trgCur = new(target);
-
- while (position < source.TotalLength)
+ private static void CopyFromCursorToSpan(ref MemoryGroupCursor source, Span target)
+ where T : struct
+ {
+ int remaining = target.Length;
+ while (remaining > 0)
{
- int fwd = Math.Min(srcCur.LookAhead(), trgCur.LookAhead());
- Span srcSpan = srcCur.GetSpan(fwd);
- Span trgSpan = trgCur.GetSpan(fwd);
- transform(srcSpan, trgSpan);
-
- srcCur.Forward(fwd);
- trgCur.Forward(fwd);
- position += fwd;
+ int copied = target.Length - remaining;
+ int fwd = Math.Min(remaining, source.LookAhead());
+ source.GetSpan(fwd).CopyTo(target[copied..]);
+ source.Forward(fwd);
+ remaining -= fwd;
}
}
- internal static void TransformInplace(
- this IMemoryGroup memoryGroup,
- TransformItemsInplaceDelegate transform)
+ private static void ForwardCursor(ref MemoryGroupCursor cursor, int steps)
where T : struct
{
- foreach (Memory memory in memoryGroup)
+ int remaining = steps;
+ while (remaining > 0)
{
- transform(memory.Span);
+ int fwd = Math.Min(remaining, cursor.LookAhead());
+ cursor.Forward(fwd);
+ remaining -= fwd;
}
}
- internal static bool IsEmpty(this IMemoryGroup group)
- where T : struct
- => group.Count == 0;
-
private struct MemoryGroupCursor
where T : struct
{
diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs
index 950e2a019..75e93ce7f 100644
--- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs
+++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs
@@ -31,23 +31,23 @@ internal abstract partial class MemoryGroup
///
[MethodImpl(InliningOptions.ShortMethod)]
- public override MemoryGroupEnumerator GetEnumerator()
- {
- return new MemoryGroupEnumerator(this);
- }
+ public override MemoryGroupEnumerator GetEnumerator() => new(this);
///
IEnumerator> IEnumerable>.GetEnumerator()
- {
+
/* The runtime sees the Array class as if it implemented the
* type-generic collection interfaces explicitly, so here we
* can just cast the source array to IList> (or to
* an equivalent type), and invoke the generic GetEnumerator
* method directly from that interface reference. This saves
* having to create our own iterator block here. */
- return ((IList>)this.source).GetEnumerator();
- }
+ => ((IList>)this.source).GetEnumerator();
- public override void Dispose() => this.View.Invalidate();
+ public override void Dispose()
+ {
+ this.View.Invalidate();
+ this.ReleaseAllocationTracking();
+ }
}
}
diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
index af896ee0e..c4b22f156 100644
--- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
+++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
@@ -60,6 +60,58 @@ internal abstract partial class MemoryGroup
}
}
+ internal override void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes)
+ {
+ if (this.groupLifetimeGuard != null)
+ {
+ // Pool-owned multi-buffer groups recover leaked handles through the group guard finalizer.
+ this.groupLifetimeGuard.AttachAllocationTracking(allocator, lengthInBytes);
+ return;
+ }
+
+ IMemoryOwner[]? memoryOwners = this.memoryOwners;
+ if (memoryOwners?.Length == 1 && memoryOwners[0] is AllocationTrackedMemoryManager trackedOwner)
+ {
+ // Single-buffer groups should release tracking with the buffer owner when that owner has
+ // a more precise lifetime, such as an existing pooled-resource finalizer.
+ trackedOwner.AttachAllocationTracking(allocator, lengthInBytes);
+ return;
+ }
+
+ if (memoryOwners?.Length > 1)
+ {
+ foreach (IMemoryOwner memoryOwner in memoryOwners)
+ {
+ if (memoryOwner is not AllocationTrackedMemoryManager)
+ {
+ // Splitting is only valid when every segment can own its reservation. A single
+ // untracked segment makes the whole group ineligible, and this preflight has
+ // not attached anything yet, so the entire group can fall back immediately.
+ base.AttachAllocationTracking(allocator, lengthInBytes);
+ return;
+ }
+ }
+
+ // Non-pool multi-buffer groups have no group-level finalizer, so each segment carries
+ // its own share of the reservation through the segment owner or its lifetime guard.
+ long remainingLengthInBytes = lengthInBytes;
+ int lastOwnerIndex = memoryOwners.Length - 1;
+ for (int i = 0; i < lastOwnerIndex; i++)
+ {
+ trackedOwner = (AllocationTrackedMemoryManager)memoryOwners[i];
+ long ownerLengthInBytes = (long)trackedOwner.Memory.Length * Unsafe.SizeOf();
+ trackedOwner.AttachAllocationTracking(allocator, ownerLengthInBytes);
+ remainingLengthInBytes -= ownerLengthInBytes;
+ }
+
+ trackedOwner = (AllocationTrackedMemoryManager)memoryOwners[lastOwnerIndex];
+ trackedOwner.AttachAllocationTracking(allocator, remainingLengthInBytes);
+ return;
+ }
+
+ base.AttachAllocationTracking(allocator, lengthInBytes);
+ }
+
private static IMemoryOwner[] CreateBuffers(
UnmanagedMemoryHandle[] pooledBuffers,
int bufferLength,
@@ -73,8 +125,8 @@ internal abstract partial class MemoryGroup
result[i] = currentBuffer;
}
- ObservedBuffer lastBuffer = ObservedBuffer.Create(pooledBuffers[pooledBuffers.Length - 1], sizeOfLastBuffer, options);
- result[result.Length - 1] = lastBuffer;
+ ObservedBuffer lastBuffer = ObservedBuffer.Create(pooledBuffers[^1], sizeOfLastBuffer, options);
+ result[^1] = lastBuffer;
return result;
}
@@ -155,6 +207,7 @@ internal abstract partial class MemoryGroup
}
}
+ this.ReleaseAllocationTracking();
this.memoryOwners = null;
this.IsValid = false;
this.groupLifetimeGuard = null;
diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs
index 6dd99fcb0..e0b9bca5e 100644
--- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs
+++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs
@@ -21,6 +21,7 @@ internal abstract partial class MemoryGroup : IMemoryGroup, IDisposable
{
private static readonly int ElementSize = Unsafe.SizeOf();
+ private AllocationTrackingState allocationTracking;
private MemoryGroupSpanCache memoryGroupSpanCache;
private MemoryGroup(int bufferLength, long totalLength)
@@ -52,16 +53,36 @@ internal abstract partial class MemoryGroup : IMemoryGroup, IDisposable
///
public abstract MemoryGroupEnumerator GetEnumerator();
+ ///
+ /// Attaches allocation tracking by specifying the allocator and the length, in bytes, to be tracked.
+ ///
+ /// The memory allocator to use for tracking allocations.
+ /// The length, in bytes, of the memory region to track. Must be greater than or equal to zero.
+ ///
+ /// Intended for one-time initialization after the group has been created; callers should avoid changing
+ /// tracking state concurrently with disposal.
+ ///
+ internal virtual void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes) =>
+ this.allocationTracking.Attach(allocator, lengthInBytes);
+
+ ///
+ /// Releases any resources or tracking information associated with allocation tracking for this instance.
+ ///
+ ///
+ /// This method is intended to be called when allocation tracking is no longer needed. It is safe
+ /// to call multiple times; subsequent calls after the first have no effect, even when called concurrently.
+ ///
+ internal void ReleaseAllocationTracking() => this.allocationTracking.Release();
+
///
IEnumerator> IEnumerable>.GetEnumerator()
- {
+
/* This method is implemented in each derived class.
* Implementing the method here as non-abstract and throwing,
* then reimplementing it explicitly in each derived class, is
* a workaround for the lack of support for abstract explicit
* interface method implementations in C#. */
- throw new NotImplementedException($"The type {this.GetType()} needs to override IEnumerable>.GetEnumerator()");
- }
+ => throw new NotImplementedException($"The type {this.GetType()} needs to override IEnumerable>.GetEnumerator()");
///
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable>)this).GetEnumerator();
@@ -81,8 +102,8 @@ internal abstract partial class MemoryGroup : IMemoryGroup, IDisposable
int bufferAlignmentInElements,
AllocationOptions options = AllocationOptions.None)
{
- int bufferCapacityInBytes = allocator.GetBufferCapacityInBytes();
Guard.NotNull(allocator, nameof(allocator));
+ int bufferCapacityInBytes = allocator.GetBufferCapacityInBytes();
if (totalLengthInElements < 0)
{
@@ -97,8 +118,8 @@ internal abstract partial class MemoryGroup : IMemoryGroup, IDisposable
if (totalLengthInElements == 0)
{
- IMemoryOwner[] buffers0 = [allocator.Allocate(0, options)];
- return new Owned(buffers0, 0, 0, true);
+ IMemoryOwner[] emptyBuffer = [allocator.AllocateGroupBuffer(0, options)];
+ return new Owned(emptyBuffer, 0, 0, true);
}
int numberOfAlignedSegments = blockCapacityInElements / bufferAlignmentInElements;
@@ -123,12 +144,12 @@ internal abstract partial class MemoryGroup : IMemoryGroup, IDisposable
IMemoryOwner[] buffers = new IMemoryOwner[bufferCount];
for (int i = 0; i < buffers.Length - 1; i++)
{
- buffers[i] = allocator.Allocate(bufferLength, options);
+ buffers[i] = allocator.AllocateGroupBuffer(bufferLength, options);
}
if (bufferCount > 0)
{
- buffers[^1] = allocator.Allocate(sizeOfLastBuffer, options);
+ buffers[^1] = allocator.AllocateGroupBuffer(sizeOfLastBuffer, options);
}
return new Owned(buffers, bufferLength, totalLengthInElements, true);
diff --git a/src/ImageSharp/Memory/InvalidMemoryOperationException.cs b/src/ImageSharp/Memory/InvalidMemoryOperationException.cs
index 81210f13d..724af35e1 100644
--- a/src/ImageSharp/Memory/InvalidMemoryOperationException.cs
+++ b/src/ImageSharp/Memory/InvalidMemoryOperationException.cs
@@ -39,4 +39,9 @@ public class InvalidMemoryOperationException : InvalidOperationException
[DoesNotReturn]
internal static void ThrowAllocationOverLimitException(ulong length, long limit) =>
throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of length={length} that exceeded the limit {limit}.");
+
+ [DoesNotReturn]
+ internal static void ThrowAccumulativeAllocationOverLimitException(long requestedLength, long totalLength, long limit) =>
+ throw new InvalidMemoryOperationException(
+ $"Attempted to allocate a buffer of length={requestedLength} that would increase the accumulative allocation size to {totalLength}, exceeding the limit {limit}.");
}
diff --git a/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs b/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs
index ff306e1e4..57ebcab76 100644
--- a/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs
+++ b/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs
@@ -29,6 +29,9 @@ public static class MemoryAllocatorExtensions
AllocationOptions options = AllocationOptions.None)
where T : struct
{
+ Guard.MustBeGreaterThan(width, 0, nameof(width));
+ Guard.MustBeGreaterThan(height, 0, nameof(height));
+
long groupLength = (long)width * height;
MemoryGroup memoryGroup;
if (preferContiguosImageBuffers && groupLength < int.MaxValue)
@@ -104,6 +107,9 @@ public static class MemoryAllocatorExtensions
AllocationOptions options = AllocationOptions.None)
where T : struct
{
+ Guard.MustBeGreaterThan(width, 0, nameof(width));
+ Guard.MustBeGreaterThan(height, 0, nameof(height));
+
long groupLength = (long)width * height;
MemoryGroup memoryGroup = memoryAllocator.AllocateGroup(
groupLength,
diff --git a/src/ImageSharp/Memory/TransformItemsDelegate{TSource, TTarget}.cs b/src/ImageSharp/Memory/TransformItemsDelegate{TSource, TTarget}.cs
deleted file mode 100644
index bc3d17f8f..000000000
--- a/src/ImageSharp/Memory/TransformItemsDelegate{TSource, TTarget}.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-namespace SixLabors.ImageSharp.Memory;
-
-#pragma warning disable SA1649 // File name should match first type name
-internal delegate void TransformItemsDelegate(ReadOnlySpan source, Span target);
-#pragma warning restore SA1649 // File name should match first type name
diff --git a/src/ImageSharp/Memory/TransformItemsInplaceDelegate.cs b/src/ImageSharp/Memory/TransformItemsInplaceDelegate.cs
deleted file mode 100644
index d1ef51fb8..000000000
--- a/src/ImageSharp/Memory/TransformItemsInplaceDelegate.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-namespace SixLabors.ImageSharp.Memory;
-
-internal delegate void TransformItemsInplaceDelegate(Span data);
diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
index d7932f90b..aa2eb29e7 100644
--- a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
+++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
@@ -318,7 +318,7 @@ public sealed class ExifProfile : IDeepCloneable
{
if (location.Value?.Length == 2)
{
- Vector2 point = TransformUtils.ProjectiveTransform2D(location.Value[0], location.Value[1], matrix);
+ Vector2 point = TransformUtilities.ProjectiveTransform2D(location.Value[0], location.Value[1], matrix);
// Ensure the point is within the image dimensions.
point = Vector2.Clamp(point, Vector2.Zero, new Vector2(width - 1, height - 1));
@@ -340,18 +340,18 @@ public sealed class ExifProfile : IDeepCloneable
if (area.Value?.Length == 4)
{
RectangleF rectangle = new(area.Value[0], area.Value[1], area.Value[2], area.Value[3]);
- if (!TransformUtils.TryGetTransformedRectangle(rectangle, matrix, out Rectangle bounds))
+ if (!TransformUtilities.TryGetTransformedRectangle(rectangle, matrix, out RectangleF bounds))
{
return;
}
// Ensure the bounds are within the image dimensions.
- bounds = Rectangle.Intersect(bounds, new Rectangle(0, 0, width, height));
+ bounds = RectangleF.Intersect(bounds, new Rectangle(0, 0, width, height));
- area.Value[0] = (ushort)bounds.X;
- area.Value[1] = (ushort)bounds.Y;
- area.Value[2] = (ushort)bounds.Width;
- area.Value[3] = (ushort)bounds.Height;
+ area.Value[0] = (ushort)MathF.Floor(bounds.X);
+ area.Value[1] = (ushort)MathF.Floor(bounds.Y);
+ area.Value[2] = (ushort)MathF.Ceiling(bounds.Width);
+ area.Value[3] = (ushort)MathF.Ceiling(bounds.Height);
this.SetValue(ExifTag.SubjectArea, area.Value);
}
else
diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs
index 732e3eab2..659df01a6 100644
--- a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs
+++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs
@@ -403,6 +403,13 @@ internal sealed class ExifWriter
return WriteUInt32((uint)longNumber, destination, offset);
}
+ // ExifLong8Array retains ulong storage but reports Long when every value fits
+ // in 32 bits, allowing BigTIFF offsets to be serialized by classic EXIF writers.
+ if (value is ulong long8Value)
+ {
+ return WriteUInt32((uint)long8Value, destination, offset);
+ }
+
return WriteUInt32((uint)value, destination, offset);
case ExifDataType.Long8:
return WriteUInt64((ulong)value, destination, offset);
diff --git a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs
index 9e89d24ff..2f1b15b6b 100644
--- a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs
+++ b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs
@@ -20,82 +20,46 @@ internal sealed partial class IccDataReader
public IccTagDataEntry ReadTagDataEntry(IccTagTableEntry info)
{
this.currentIndex = (int)info.Offset;
- switch (this.ReadTagDataEntryHeader())
- {
- case IccTypeSignature.Chromaticity:
- return this.ReadChromaticityTagDataEntry();
- case IccTypeSignature.ColorantOrder:
- return this.ReadColorantOrderTagDataEntry();
- case IccTypeSignature.ColorantTable:
- return this.ReadColorantTableTagDataEntry();
- case IccTypeSignature.Curve:
- return this.ReadCurveTagDataEntry();
- case IccTypeSignature.Data:
- return this.ReadDataTagDataEntry(info.DataSize);
- case IccTypeSignature.DateTime:
- return this.ReadDateTimeTagDataEntry();
- case IccTypeSignature.Lut16:
- return this.ReadLut16TagDataEntry();
- case IccTypeSignature.Lut8:
- return this.ReadLut8TagDataEntry();
- case IccTypeSignature.LutAToB:
- return this.ReadLutAtoBTagDataEntry();
- case IccTypeSignature.LutBToA:
- return this.ReadLutBtoATagDataEntry();
- case IccTypeSignature.Measurement:
- return this.ReadMeasurementTagDataEntry();
- case IccTypeSignature.MultiLocalizedUnicode:
- return this.ReadMultiLocalizedUnicodeTagDataEntry();
- case IccTypeSignature.MultiProcessElements:
- return this.ReadMultiProcessElementsTagDataEntry();
- case IccTypeSignature.NamedColor2:
- return this.ReadNamedColor2TagDataEntry();
- case IccTypeSignature.ParametricCurve:
- return this.ReadParametricCurveTagDataEntry();
- case IccTypeSignature.ProfileSequenceDesc:
- return this.ReadProfileSequenceDescTagDataEntry();
- case IccTypeSignature.ProfileSequenceIdentifier:
- return this.ReadProfileSequenceIdentifierTagDataEntry();
- case IccTypeSignature.ResponseCurveSet16:
- return this.ReadResponseCurveSet16TagDataEntry();
- case IccTypeSignature.S15Fixed16Array:
- return this.ReadFix16ArrayTagDataEntry(info.DataSize);
- case IccTypeSignature.Signature:
- return this.ReadSignatureTagDataEntry();
- case IccTypeSignature.Text:
- return this.ReadTextTagDataEntry(info.DataSize);
- case IccTypeSignature.U16Fixed16Array:
- return this.ReadUFix16ArrayTagDataEntry(info.DataSize);
- case IccTypeSignature.UInt16Array:
- return this.ReadUInt16ArrayTagDataEntry(info.DataSize);
- case IccTypeSignature.UInt32Array:
- return this.ReadUInt32ArrayTagDataEntry(info.DataSize);
- case IccTypeSignature.UInt64Array:
- return this.ReadUInt64ArrayTagDataEntry(info.DataSize);
- case IccTypeSignature.UInt8Array:
- return this.ReadUInt8ArrayTagDataEntry(info.DataSize);
- case IccTypeSignature.ViewingConditions:
- return this.ReadViewingConditionsTagDataEntry();
- case IccTypeSignature.Xyz:
- return this.ReadXyzTagDataEntry(info.DataSize);
+ return this.ReadTagDataEntryHeader() switch
+ {
+ IccTypeSignature.Chromaticity => this.ReadChromaticityTagDataEntry(),
+ IccTypeSignature.ColorantOrder => this.ReadColorantOrderTagDataEntry(),
+ IccTypeSignature.ColorantTable => this.ReadColorantTableTagDataEntry(),
+ IccTypeSignature.Curve => this.ReadCurveTagDataEntry(),
+ IccTypeSignature.Data => this.ReadDataTagDataEntry(info.DataSize),
+ IccTypeSignature.DateTime => this.ReadDateTimeTagDataEntry(),
+ IccTypeSignature.Lut16 => this.ReadLut16TagDataEntry(),
+ IccTypeSignature.Lut8 => this.ReadLut8TagDataEntry(),
+ IccTypeSignature.LutAToB => this.ReadLutAtoBTagDataEntry(),
+ IccTypeSignature.LutBToA => this.ReadLutBtoATagDataEntry(),
+ IccTypeSignature.Measurement => this.ReadMeasurementTagDataEntry(),
+ IccTypeSignature.MultiLocalizedUnicode => this.ReadMultiLocalizedUnicodeTagDataEntry(),
+ IccTypeSignature.MultiProcessElements => this.ReadMultiProcessElementsTagDataEntry(),
+ IccTypeSignature.NamedColor2 => this.ReadNamedColor2TagDataEntry(),
+ IccTypeSignature.ParametricCurve => this.ReadParametricCurveTagDataEntry(),
+ IccTypeSignature.ProfileSequenceDesc => this.ReadProfileSequenceDescTagDataEntry(),
+ IccTypeSignature.ProfileSequenceIdentifier => this.ReadProfileSequenceIdentifierTagDataEntry(),
+ IccTypeSignature.ResponseCurveSet16 => this.ReadResponseCurveSet16TagDataEntry(),
+ IccTypeSignature.S15Fixed16Array => this.ReadFix16ArrayTagDataEntry(info.DataSize),
+ IccTypeSignature.Signature => this.ReadSignatureTagDataEntry(),
+ IccTypeSignature.Text => this.ReadTextTagDataEntry(info.DataSize),
+ IccTypeSignature.U16Fixed16Array => this.ReadUFix16ArrayTagDataEntry(info.DataSize),
+ IccTypeSignature.UInt16Array => this.ReadUInt16ArrayTagDataEntry(info.DataSize),
+ IccTypeSignature.UInt32Array => this.ReadUInt32ArrayTagDataEntry(info.DataSize),
+ IccTypeSignature.UInt64Array => this.ReadUInt64ArrayTagDataEntry(info.DataSize),
+ IccTypeSignature.UInt8Array => this.ReadUInt8ArrayTagDataEntry(info.DataSize),
+ IccTypeSignature.ViewingConditions => this.ReadViewingConditionsTagDataEntry(),
+ IccTypeSignature.Xyz => this.ReadXyzTagDataEntry(info.DataSize),
// V2 Types:
- case IccTypeSignature.TextDescription:
- return this.ReadTextDescriptionTagDataEntry();
- case IccTypeSignature.CrdInfo:
- return this.ReadCrdInfoTagDataEntry();
- case IccTypeSignature.Screening:
- return this.ReadScreeningTagDataEntry();
- case IccTypeSignature.UcrBg:
- return this.ReadUcrBgTagDataEntry(info.DataSize);
+ IccTypeSignature.TextDescription => this.ReadTextDescriptionTagDataEntry(),
+ IccTypeSignature.CrdInfo => this.ReadCrdInfoTagDataEntry(),
+ IccTypeSignature.Screening => this.ReadScreeningTagDataEntry(),
+ IccTypeSignature.UcrBg => this.ReadUcrBgTagDataEntry(info.DataSize),
// Unsupported or unknown
- case IccTypeSignature.DeviceSettings:
- case IccTypeSignature.NamedColor:
- case IccTypeSignature.Unknown:
- default:
- return this.ReadUnknownTagDataEntry(info.DataSize);
- }
+ _ => this.ReadUnknownTagDataEntry(info.DataSize),
+ };
}
///
@@ -477,7 +441,7 @@ internal sealed partial class IccDataReader
return new IccMultiLocalizedUnicodeTagDataEntry(text);
- CultureInfo ReadCulture(string language, string country)
+ static CultureInfo ReadCulture(string language, string country)
{
if (string.IsNullOrWhiteSpace(language))
{
diff --git a/src/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.TagDataEntry.cs b/src/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.TagDataEntry.cs
index 6019a0bff..2cf637654 100644
--- a/src/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.TagDataEntry.cs
+++ b/src/ImageSharp/Metadata/Profiles/ICC/DataWriter/IccDataWriter.TagDataEntry.cs
@@ -478,7 +478,7 @@ internal sealed partial class IccDataWriter
this.dataStream.Position += cultureCount * 12;
// TODO: Investigate cost of Linq GroupBy
- IGrouping[] texts = value.Texts.GroupBy(t => t.Text).ToArray();
+ IGrouping[] texts = [.. value.Texts.GroupBy(t => t.Text)];
uint[] offset = new uint[texts.Length];
int[] lengths = new int[texts.Length];
diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.SRGB.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.SRGB.cs
new file mode 100644
index 000000000..bfa4ab9bd
--- /dev/null
+++ b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.SRGB.cs
@@ -0,0 +1,346 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using SixLabors.ImageSharp.ColorProfiles;
+
+namespace SixLabors.ImageSharp.Metadata.Profiles.Icc;
+
+///
+/// Provides logic for identifying canonical IEC 61966-2-1 (sRGB) matrix-TRC ICC profiles,
+/// distinguishing them from appearance or device-specific variants.
+///
+public sealed partial class IccProfile
+{
+ // sRGB v2 Preference
+ private static readonly IccProfileId StandardRgbV2 = new(0x3D0EB2DE, 0xAE9397BE, 0x9B6726CE, 0x8C0A43CE);
+
+ // sRGB v4 Preference
+ private static readonly IccProfileId StandardRgbV4 = new(0x34562ABF, 0x994CCD06, 0x6D2C5721, 0xD0D68C5D);
+
+ ///
+ /// Detects canonical sRGB matrix+TRC profiles quickly and safely.
+ /// Rules:
+ /// 1) Accept known IEC sRGB v2 and v4 by profile ID.
+ /// 2) Require RGB, PCS=XYZ, ICC v2 or v4, and no A2B*/B2A* LUTs.
+ /// 3) Require rTRC, gTRC, bTRC to exist and be identical by parameters or sampled shape.
+ /// 4) Accept if rXYZ/gXYZ/bXYZ already match the D50-adapted sRGB colorants within tolerance.
+ /// 5) If white point ≈ D65, adapt only the colorant columns to D50 using Bradford
+ /// via and then compare.
+ /// This rejects channel-swapped and appearance profiles while allowing real sRGB.
+ ///
+ ///
+ /// Reference D50-adapted sRGB colorants from Bruce Lindbloom:
+ ///
+ /// R=(0.4360747, 0.2225045, 0.0139322)
+ /// G=(0.3850649, 0.7168786, 0.0971045)
+ /// B=(0.1430804, 0.0606169, 0.7141733)
+ ///
+ internal bool IsCanonicalSrgbMatrixTrc()
+ {
+ IccProfileHeader h = this.Header;
+
+ // Fast path for known IEC sRGB profile IDs
+ if (h.Id == StandardRgbV2 || h.Id == StandardRgbV4)
+ {
+ return true;
+ }
+
+ // Header gating to avoid parsing work for obvious non-matches
+ if (h.FileSignature != "acsp")
+ {
+ return false;
+ }
+
+ if (h.DataColorSpace != IccColorSpaceType.Rgb)
+ {
+ return false;
+ }
+
+ if (h.ProfileConnectionSpace != IccColorSpaceType.CieXyz)
+ {
+ return false;
+ }
+
+ if (h.Version.Major is not 2 and not 4)
+ {
+ return false;
+ }
+
+ this.InitializeEntries();
+ IccTagDataEntry[] entries = this.entries;
+
+ // Reject device/display LUT profiles. We only accept matrix+TRC encodings.
+ if (Has(entries, IccProfileTag.AToB0) || Has(entries, IccProfileTag.AToB1) || Has(entries, IccProfileTag.AToB2) ||
+ Has(entries, IccProfileTag.BToA0) || Has(entries, IccProfileTag.BToA1) || Has(entries, IccProfileTag.BToA2))
+ {
+ return false;
+ }
+
+ // Required matrix+TRC tags
+ if (!TryGetXyz(entries, IccProfileTag.MediaWhitePoint, out Vector3 wtpt))
+ {
+ return false;
+ }
+
+ if (!TryGetXyz(entries, IccProfileTag.RedMatrixColumn, out Vector3 rXYZ))
+ {
+ return false;
+ }
+
+ if (!TryGetXyz(entries, IccProfileTag.GreenMatrixColumn, out Vector3 gXYZ))
+ {
+ return false;
+ }
+
+ if (!TryGetXyz(entries, IccProfileTag.BlueMatrixColumn, out Vector3 bXYZ))
+ {
+ return false;
+ }
+
+ // TRCs must exist and be identical across channels. This filters many trick profiles.
+ if (!TryGetTrc(entries, IccProfileTag.RedTrc, out Trc tR))
+ {
+ return false;
+ }
+
+ if (!TryGetTrc(entries, IccProfileTag.GreenTrc, out Trc tG))
+ {
+ return false;
+ }
+
+ if (!TryGetTrc(entries, IccProfileTag.BlueTrc, out Trc tB))
+ {
+ return false;
+ }
+
+ if (!tR.Equals(tG) || !tR.Equals(tB))
+ {
+ return false;
+ }
+
+ // D50-adapted sRGB colorants (compare as columns: r,g,b), tight epsilon
+ const float eps = 2e-3F;
+ Vector3 rRef = new(0.4360747F, 0.2225045F, 0.0139322F);
+ Vector3 gRef = new(0.3850649F, 0.7168786F, 0.0971045F);
+ Vector3 bRef = new(0.1430804F, 0.0606169F, 0.7141733F);
+
+ // First, accept if the stored colorants are already the D50 sRGB primaries.
+ // Many v2 sRGB profiles store D50-adapted colorants while declaring wtpt≈D65.
+ if (Near(rXYZ, rRef, eps) && Near(gXYZ, gRef, eps) && Near(bXYZ, bRef, eps))
+ {
+ return true;
+ }
+
+ // If the profile declares a D65 white, adapt the colorant columns to D50 and compare again.
+ // We never adapt when they already match, to avoid compounding rounding.
+ if (Near(wtpt, KnownIlluminants.D65.AsVector3Unsafe(), 2e-3F))
+ {
+ CieXyz fromWp = new(wtpt); // Declared white
+ CieXyz toWp = KnownIlluminants.D50; // PCS white
+ Matrix4x4 matrix = KnownChromaticAdaptationMatrices.Bradford;
+
+ rXYZ = VonKriesChromaticAdaptation.Transform(new CieXyz(rXYZ), (fromWp, toWp), matrix).AsVector3Unsafe();
+ gXYZ = VonKriesChromaticAdaptation.Transform(new CieXyz(gXYZ), (fromWp, toWp), matrix).AsVector3Unsafe();
+ bXYZ = VonKriesChromaticAdaptation.Transform(new CieXyz(bXYZ), (fromWp, toWp), matrix).AsVector3Unsafe();
+ }
+
+ // Require identity mapping of primaries, no permutation
+ if (!Near(rXYZ, rRef, eps) || !Near(gXYZ, gRef, eps) || !Near(bXYZ, bRef, eps))
+ {
+ return false;
+ }
+
+ return true;
+
+ static bool Has(ReadOnlySpan span, IccProfileTag tag)
+ {
+ for (int i = 0; i < span.Length; i++)
+ {
+ if (span[i].TagSignature == tag)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ static bool TryGetXyz(ReadOnlySpan span, IccProfileTag tag, out Vector3 xyz)
+ {
+ for (int i = 0; i < span.Length; i++)
+ {
+ IccTagDataEntry e = span[i];
+ if (e.TagSignature != tag)
+ {
+ continue;
+ }
+
+ if (e is IccXyzTagDataEntry x && x.Data is { Length: >= 1 })
+ {
+ xyz = x.Data[0];
+ return true;
+ }
+
+ break;
+ }
+
+ xyz = default;
+ return false;
+ }
+
+ static bool TryGetTrc(ReadOnlySpan span, IccProfileTag tag, out Trc trc)
+ {
+ for (int i = 0; i < span.Length; i++)
+ {
+ IccTagDataEntry e = span[i];
+ if (e.TagSignature != tag)
+ {
+ continue;
+ }
+
+ if (e is IccParametricCurveTagDataEntry p)
+ {
+ trc = Trc.FromParametric(p.Curve);
+ return true;
+ }
+
+ if (e is IccCurveTagDataEntry c)
+ {
+ trc = Trc.FromCurveLut(c.CurveData);
+ return true;
+ }
+
+ break;
+ }
+
+ trc = default;
+ return false;
+ }
+
+ static bool Near(in Vector3 a, in Vector3 b, float tol)
+ => MathF.Abs(a.X - b.X) <= tol &&
+ MathF.Abs(a.Y - b.Y) <= tol &&
+ MathF.Abs(a.Z - b.Z) <= tol;
+ }
+
+ ///
+ /// Compact, allocation-free descriptor of a TRC for equality and optional sRGB check.
+ ///
+ private readonly struct Trc : IEquatable
+ {
+ private readonly byte kind; // 0 = none, 1 = parametric, 2 = sampled
+ private readonly float g; // parametric payload or downsampled hash
+ private readonly float a;
+ private readonly float b;
+ private readonly float c;
+ private readonly float d;
+ private readonly float e;
+ private readonly float f;
+ private readonly int n; // for sampled, length or a small signature
+
+ private Trc(byte kind, float g, float a, float b, float c, float d, float e, float f, int n)
+ {
+ this.kind = kind;
+ this.g = g;
+ this.a = a;
+ this.b = b;
+ this.c = c;
+ this.d = d;
+ this.e = e;
+ this.f = f;
+ this.n = n;
+ }
+
+ public static Trc FromParametric(IccParametricCurve c)
+
+ // Normalize by curve type to a stable tuple
+ // The types map to piecewise forms, but equality across channels is the key requirement here
+ => new(1, c.G, c.A, c.B, c.C, c.D, c.E, c.F, (int)c.Type);
+
+ public static Trc FromCurveLut(float[] data)
+ {
+ // Exact sequence equality is enforced by the calling code using the same Trc construction
+ // Record a short signature to compare cheaply, avoid copying
+ if (data == null)
+ {
+ return default;
+ }
+
+ int n = data.Length;
+ if (n == 0)
+ {
+ return default;
+ }
+
+ // Downsample a few points to a robust fingerprint
+ // Use fixed indices to avoid allocations
+ float s0 = data[0];
+ float s1 = data[n >> 2];
+ float s2 = data[n >> 1];
+ float s3 = data[(n * 3) >> 2];
+ float s4 = data[n - 1];
+
+ return new Trc(
+ 2,
+ s0,
+ s1,
+ s2,
+ s3,
+ s4,
+ 0F,
+ 0F,
+ n);
+ }
+
+ public override bool Equals(object? obj) => obj is Trc trc && this.Equals(trc);
+
+ public bool Equals(Trc other)
+ {
+ if (this.kind != other.kind)
+ {
+ return false;
+ }
+
+ if (this.kind == 0)
+ {
+ return false;
+ }
+
+ if (this.kind == 1)
+ {
+ // parametric: exact parameter match and type match
+ return this.n == other.n &&
+ this.g == other.g && this.a == other.a &&
+ this.b == other.b && this.c == other.c &&
+ this.d == other.d && this.e == other.e && this.f == other.f;
+ }
+
+ // sampled: same length and same 5-point fingerprint
+ return this.n == other.n &&
+ this.g == other.g && this.a == other.a &&
+ this.b == other.b && this.c == other.c && this.d == other.d;
+ }
+
+ // Optional stricter sRGB check if you need it later
+ public bool IsSrgbLike()
+ {
+ if (this.kind == 1)
+ {
+ // Accept common sRGB parametric encodings where type and parameters match
+ // IEC 61966-2-1 maps to Type4 or Type5 forms in practice
+ // Tighten only if you must exclude gamma~2.2 profiles that share primaries
+ return true;
+ }
+
+ return true;
+ }
+
+ public override int GetHashCode()
+ {
+ int a = HashCode.Combine(this.kind, this.g, this.a, this.b, this.c, this.d, this.e);
+ int b = HashCode.Combine(this.f, this.n);
+ return HashCode.Combine(a, b);
+ }
+ }
+}
diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs
index 392ccb306..eaba0a045 100644
--- a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs
+++ b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs
@@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Icc;
///
/// Represents an ICC profile
///
-public sealed class IccProfile : IDeepCloneable
+public sealed partial class IccProfile : IDeepCloneable
{
///
/// The byte array to read the ICC profile from
@@ -110,8 +110,8 @@ public sealed class IccProfile : IDeepCloneable
// need to copy some values because they need to be zero for the hashing
Span temp = stackalloc byte[24];
data.AsSpan(profileFlagPos, 4).CopyTo(temp);
- data.AsSpan(renderingIntentPos, 4).CopyTo(temp.Slice(4));
- data.AsSpan(profileIdPos, 16).CopyTo(temp.Slice(8));
+ data.AsSpan(renderingIntentPos, 4).CopyTo(temp[4..]);
+ data.AsSpan(profileIdPos, 16).CopyTo(temp[8..]);
try
{
@@ -131,7 +131,7 @@ public sealed class IccProfile : IDeepCloneable
}
finally
{
- temp.Slice(0, 4).CopyTo(data.AsSpan(profileFlagPos));
+ temp[..4].CopyTo(data.AsSpan(profileFlagPos));
temp.Slice(4, 4).CopyTo(data.AsSpan(renderingIntentPos));
temp.Slice(8, 16).CopyTo(data.AsSpan(profileIdPos));
}
diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs
index b50885d02..959668aaf 100644
--- a/src/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs
+++ b/src/ImageSharp/Metadata/Profiles/ICC/IccProfileHeader.cs
@@ -11,17 +11,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Icc;
///
public sealed class IccProfileHeader
{
- private static readonly Vector3 TruncatedD50 = new(0.9642029F, 1F, 0.8249054F);
-
- // sRGB v2 Preference
- private static readonly IccProfileId StandardRgbV2 = new(0x3D0EB2DE, 0xAE9397BE, 0x9B6726CE, 0x8C0A43CE);
-
- // sRGB v4 Preference
- private static readonly IccProfileId StandardRgbV4 = new(0x34562ABF, 0x994CCD06, 0x6D2C5721, 0xD0D68C5D);
-
- // sRGB v4 Appearance
- private static readonly IccProfileId StandardRgbV4A = new(0xDF1132A1, 0x746E97B0, 0xAD85719, 0xBE711E08);
-
///
/// Gets or sets the profile size in bytes (will be ignored when writing a profile).
///
@@ -108,31 +97,4 @@ public sealed class IccProfileHeader
/// Gets or sets the profile ID (hash).
///
public IccProfileId Id { get; set; }
-
- internal static bool IsLikelySrgb(IccProfileHeader header)
- {
- // Reject known perceptual-appearance profile
- // This profile employs perceptual rendering intents to maintain color appearance across different
- // devices and media, which can lead to variations from standard sRGB representations.
- if (header.Id == StandardRgbV4A)
- {
- return false;
- }
-
- // Accept known sRGB profile IDs
- if (header.Id == StandardRgbV2 || header.Id == StandardRgbV4)
- {
- return true;
- }
-
- // Fallback: best-guess heuristic
- return
- header.FileSignature == "acsp" &&
- header.DataColorSpace == IccColorSpaceType.Rgb &&
- (header.ProfileConnectionSpace == IccColorSpaceType.CieXyz || header.ProfileConnectionSpace == IccColorSpaceType.CieLab) &&
- (header.Class == IccProfileClass.DisplayDevice || header.Class == IccProfileClass.ColorSpace) &&
- header.PcsIlluminant == TruncatedD50 &&
- (header.Version.Major == 2 || header.Version.Major == 4) &&
- !string.Equals(header.CmmType, "ADBE", StringComparison.Ordinal);
- }
}
diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs
index 084ec388d..5e1c1942a 100644
--- a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs
+++ b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs
@@ -102,7 +102,7 @@ internal sealed class IccReader
entries.Add(entry);
}
- return entries.ToArray();
+ return [.. entries];
}
private static IccTagTableEntry[] ReadTagTable(IccDataReader reader)
@@ -132,6 +132,6 @@ internal sealed class IccReader
}
}
- return table.ToArray();
+ return [.. table];
}
}
diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccWriter.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccWriter.cs
index 6bc0560f0..4a88a76f6 100644
--- a/src/ImageSharp/Metadata/Profiles/ICC/IccWriter.cs
+++ b/src/ImageSharp/Metadata/Profiles/ICC/IccWriter.cs
@@ -81,6 +81,6 @@ internal sealed class IccWriter
}
}
- return table.ToArray();
+ return [.. table];
}
}
diff --git a/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccChromaticityTagDataEntry.cs b/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccChromaticityTagDataEntry.cs
index 1938ad630..0f61857ba 100644
--- a/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccChromaticityTagDataEntry.cs
+++ b/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccChromaticityTagDataEntry.cs
@@ -140,7 +140,7 @@ internal sealed class IccChromaticityTagDataEntry : IccTagDataEntry, IEquatable<
[0.155, 0.070]
];
default:
- throw new ArgumentException("Unrecognized colorant encoding");
+ throw new InvalidIccProfileException("Unrecognized colorant encoding");
}
}
diff --git a/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs b/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs
index 9bf323263..77bc45bd4 100644
--- a/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs
+++ b/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs
@@ -64,44 +64,7 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable
@@ -165,7 +128,7 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable
+ /// Compares two curve arrays, treating consistently.
+ ///
private static bool EqualsCurve(IccTagDataEntry[] thisCurves, IccTagDataEntry[] entryCurves)
{
bool thisNull = thisCurves is null;
@@ -202,7 +168,7 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable this.CurveB != null
- && this.Matrix3x3 != null
- && this.Matrix3x1 != null
- && this.CurveM != null
- && this.ClutValues != null
- && this.CurveA != null;
+ ///
+ /// Validates the configured processing stages and derives the external channel counts.
+ ///
+ ///
+ /// Stages are evaluated in ICC mAB order: A, CLUT, M, Matrix, B.
+ /// Sparse pipelines are valid as long as adjacent stages agree on channel counts.
+ ///
+ private (int InputChannelCount, int OutputChannelCount) GetChannelCounts()
+ {
+ // There are at most five possible mAB stages: A, CLUT, M, Matrix, and B.
+ List<(int Input, int Output, string Name)> stages = new(5);
- private bool IsMMatrixB()
- => this.CurveB != null
- && this.Matrix3x3 != null
- && this.Matrix3x1 != null
- && this.CurveM != null;
+ if (this.CurveA != null)
+ {
+ Guard.MustBeBetweenOrEqualTo(this.CurveA.Length, 1, 15, nameof(this.CurveA));
+ stages.Add((this.CurveA.Length, this.CurveA.Length, nameof(this.CurveA)));
+ }
- private bool IsAClutB()
- => this.CurveB != null
- && this.ClutValues != null
- && this.CurveA != null;
+ if (this.ClutValues != null)
+ {
+ stages.Add((this.ClutValues.InputChannelCount, this.ClutValues.OutputChannelCount, nameof(this.ClutValues)));
+ }
- private bool IsB() => this.CurveB != null;
+ if (this.CurveM != null)
+ {
+ Guard.MustBeBetweenOrEqualTo(this.CurveM.Length, 1, 15, nameof(this.CurveM));
+ stages.Add((this.CurveM.Length, this.CurveM.Length, nameof(this.CurveM)));
+ }
+
+ if (this.Matrix3x3 != null || this.Matrix3x1 != null)
+ {
+ Guard.IsTrue(this.Matrix3x3 != null && this.Matrix3x1 != null, nameof(this.Matrix3x3), "Matrix must include both the 3x3 and 3x1 components");
+ stages.Add((3, 3, nameof(this.Matrix3x3)));
+ }
+ if (this.CurveB != null)
+ {
+ Guard.MustBeBetweenOrEqualTo(this.CurveB.Length, 1, 15, nameof(this.CurveB));
+ stages.Add((this.CurveB.Length, this.CurveB.Length, nameof(this.CurveB)));
+ }
+
+ Guard.IsTrue(stages.Count > 0, nameof(this.CurveB), "AToB tag must contain at least one processing element");
+
+ for (int i = 1; i < stages.Count; i++)
+ {
+ Guard.IsTrue(
+ stages[i - 1].Output == stages[i].Input,
+ stages[i].Name,
+ $"Output channel count of {stages[i - 1].Name} does not match input channel count of {stages[i].Name}");
+ }
+
+ return (stages[0].Input, stages[^1].Output);
+ }
+
+ ///
+ /// Verifies that every supplied curve entry is a supported one-dimensional curve type.
+ ///
private void VerifyCurve(IccTagDataEntry[] curves, string name)
{
if (curves != null)
@@ -240,6 +242,9 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable
+ /// Verifies the dimensions of the optional matrix components.
+ ///
private static void VerifyMatrix(float[,] matrix3x3, float[] matrix3x1)
{
if (matrix3x1 != null)
@@ -254,6 +259,9 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable
+ /// Creates the one-dimensional matrix vector when present.
+ ///
private static Vector3? CreateMatrix3x1(float[] matrix)
{
if (matrix is null)
@@ -264,6 +272,9 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable
+ /// Creates the three-by-three matrix when present.
+ ///
private static Matrix4x4? CreateMatrix3x3(float[,] matrix)
{
if (matrix is null)
diff --git a/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutBToATagDataEntry.cs b/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutBToATagDataEntry.cs
index 033b80989..37e7b408d 100644
--- a/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutBToATagDataEntry.cs
+++ b/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutBToATagDataEntry.cs
@@ -64,44 +64,7 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable
@@ -165,7 +128,7 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable
+ /// Compares two curve arrays, treating consistently.
+ ///
private static bool EqualsCurve(IccTagDataEntry[] thisCurves, IccTagDataEntry[] entryCurves)
{
bool thisNull = thisCurves is null;
@@ -201,7 +167,7 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable this.CurveB != null && this.Matrix3x3 != null && this.Matrix3x1 != null && this.CurveM != null && this.ClutValues != null && this.CurveA != null;
+ ///
+ /// Validates the configured processing stages and derives the external channel counts.
+ ///
+ ///
+ /// Stages are evaluated in ICC mBA order: B, Matrix, M, CLUT, A.
+ /// Sparse pipelines are valid as long as adjacent stages agree on channel counts.
+ ///
+ private (int InputChannelCount, int OutputChannelCount) GetChannelCounts()
+ {
+ // There are at most five possible mBA stages: B, Matrix, M, CLUT, and A.
+ List<(int Input, int Output, string Name)> stages = new(5);
- private bool IsBMatrixM()
- => this.CurveB != null && this.Matrix3x3 != null && this.Matrix3x1 != null && this.CurveM != null;
+ if (this.CurveB != null)
+ {
+ Guard.MustBeBetweenOrEqualTo(this.CurveB.Length, 1, 15, nameof(this.CurveB));
+ stages.Add((this.CurveB.Length, this.CurveB.Length, nameof(this.CurveB)));
+ }
- private bool IsBClutA()
- => this.CurveB != null && this.ClutValues != null && this.CurveA != null;
+ if (this.Matrix3x3 != null || this.Matrix3x1 != null)
+ {
+ Guard.IsTrue(this.Matrix3x3 != null && this.Matrix3x1 != null, nameof(this.Matrix3x3), "Matrix must include both the 3x3 and 3x1 components");
+ stages.Add((3, 3, nameof(this.Matrix3x3)));
+ }
- private bool IsB() => this.CurveB != null;
+ if (this.CurveM != null)
+ {
+ Guard.MustBeBetweenOrEqualTo(this.CurveM.Length, 1, 15, nameof(this.CurveM));
+ stages.Add((this.CurveM.Length, this.CurveM.Length, nameof(this.CurveM)));
+ }
+
+ if (this.ClutValues != null)
+ {
+ stages.Add((this.ClutValues.InputChannelCount, this.ClutValues.OutputChannelCount, nameof(this.ClutValues)));
+ }
+ if (this.CurveA != null)
+ {
+ Guard.MustBeBetweenOrEqualTo(this.CurveA.Length, 1, 15, nameof(this.CurveA));
+ stages.Add((this.CurveA.Length, this.CurveA.Length, nameof(this.CurveA)));
+ }
+
+ Guard.IsTrue(stages.Count > 0, nameof(this.CurveB), "BToA tag must contain at least one processing element");
+
+ for (int i = 1; i < stages.Count; i++)
+ {
+ Guard.IsTrue(
+ stages[i - 1].Output == stages[i].Input,
+ stages[i].Name,
+ $"Output channel count of {stages[i - 1].Name} does not match input channel count of {stages[i].Name}");
+ }
+
+ return (stages[0].Input, stages[^1].Output);
+ }
+
+ ///
+ /// Verifies that every supplied curve entry is a supported one-dimensional curve type.
+ ///
private void VerifyCurve(IccTagDataEntry[] curves, string name)
{
if (curves != null)
@@ -229,6 +241,9 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable
+ /// Verifies the dimensions of the optional matrix components.
+ ///
private static void VerifyMatrix(float[,] matrix3x3, float[] matrix3x1)
{
if (matrix3x1 != null)
@@ -243,6 +258,9 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable
+ /// Creates the one-dimensional matrix vector when present.
+ ///
private static Vector3? CreateMatrix3x1(float[] matrix)
{
if (matrix is null)
@@ -253,6 +271,9 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable
+ /// Creates the three-by-three matrix when present.
+ ///
private static Matrix4x4? CreateMatrix3x3(float[,] matrix)
{
if (matrix is null)
diff --git a/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccTextDescriptionTagDataEntry.cs b/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccTextDescriptionTagDataEntry.cs
index 7db26e5c5..5f9496140 100644
--- a/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccTextDescriptionTagDataEntry.cs
+++ b/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccTextDescriptionTagDataEntry.cs
@@ -102,7 +102,7 @@ internal sealed class IccTextDescriptionTagDataEntry : IccTagDataEntry, IEquatab
localString = new IccLocalizedString(string.Empty);
}
- return new IccMultiLocalizedUnicodeTagDataEntry(new[] { localString }, textEntry.TagSignature);
+ return new IccMultiLocalizedUnicodeTagDataEntry([localString], textEntry.TagSignature);
static CultureInfo GetCulture(uint value)
{
diff --git a/src/ImageSharp/Metadata/Profiles/ICC/Various/IccLut.cs b/src/ImageSharp/Metadata/Profiles/ICC/Various/IccLut.cs
index 5f07e5589..fb47409be 100644
--- a/src/ImageSharp/Metadata/Profiles/ICC/Various/IccLut.cs
+++ b/src/ImageSharp/Metadata/Profiles/ICC/Various/IccLut.cs
@@ -23,13 +23,8 @@ internal readonly struct IccLut : IEquatable
{
Guard.NotNull(values, nameof(values));
- const float max = ushort.MaxValue;
-
this.Values = new float[values.Length];
- for (int i = 0; i < values.Length; i++)
- {
- this.Values[i] = values[i] / max;
- }
+ IccLutNormalizer.Normalize(values, this.Values);
}
///
@@ -40,13 +35,8 @@ internal readonly struct IccLut : IEquatable
{
Guard.NotNull(values, nameof(values));
- const float max = byte.MaxValue;
-
this.Values = new float[values.Length];
- for (int i = 0; i < values.Length; i++)
- {
- this.Values[i] = values[i] / max;
- }
+ IccLutNormalizer.Normalize(values, this.Values);
}
///
diff --git a/src/ImageSharp/Metadata/Profiles/ICC/Various/IccLutNormalizer.cs b/src/ImageSharp/Metadata/Profiles/ICC/Various/IccLutNormalizer.cs
new file mode 100644
index 000000000..67796cad3
--- /dev/null
+++ b/src/ImageSharp/Metadata/Profiles/ICC/Various/IccLutNormalizer.cs
@@ -0,0 +1,253 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.Intrinsics;
+
+namespace SixLabors.ImageSharp.Metadata.Profiles.Icc;
+
+///
+/// Converts integer ICC lookup-table entries to their normalized single-precision representation.
+///
+internal static class IccLutNormalizer
+{
+ ///
+ /// Defines the scalar and SIMD conversion for an integer lookup-table element type.
+ ///
+ /// The integer element type.
+ private interface INormalizeOperator
+ where T : unmanaged
+ {
+ ///
+ /// Gets the divisor that maps the integer range to [0, 1].
+ ///
+ public static abstract float Divisor { get; }
+
+ ///
+ /// Converts one scalar value.
+ ///
+ /// The integer value.
+ /// The normalized value.
+ public static abstract float Invoke(T source);
+
+ ///
+ /// Converts one 128-bit input vector and stores the expanded single-precision results.
+ ///
+ /// The packed integer values.
+ /// The normalization divisor.
+ /// The first destination element.
+ public static abstract void Invoke(Vector128 source, Vector128 divisor, ref float destination);
+
+ ///
+ /// Converts one 256-bit input vector and stores the expanded single-precision results.
+ ///
+ /// The packed integer values.
+ /// The normalization divisor.
+ /// The first destination element.
+ public static abstract void Invoke(Vector256 source, Vector256 divisor, ref float destination);
+
+ ///
+ /// Converts one 512-bit input vector and stores the expanded single-precision results.
+ ///
+ /// The packed integer values.
+ /// The normalization divisor.
+ /// The first destination element.
+ public static abstract void Invoke(Vector512 source, Vector512 divisor, ref float destination);
+ }
+
+ ///
+ /// Converts byte lookup-table entries to normalized single-precision values.
+ ///
+ /// The integer lookup-table entries.
+ /// The normalized destination values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Normalize(ReadOnlySpan source, Span destination)
+ => Normalize(source, destination);
+
+ ///
+ /// Converts unsigned-short lookup-table entries to normalized single-precision values.
+ ///
+ /// The integer lookup-table entries.
+ /// The normalized destination values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Normalize(ReadOnlySpan source, Span destination)
+ => Normalize(source, destination);
+
+ ///
+ /// Converts an integer lookup table using the widest available portable SIMD width, followed by narrower
+ /// widths and a scalar remainder.
+ ///
+ /// The integer element type.
+ /// The conversion implementation.
+ /// The integer lookup-table entries.
+ /// The normalized destination values.
+ private static void Normalize(ReadOnlySpan source, Span destination)
+ where T : unmanaged
+ where TOperator : struct, INormalizeOperator
+ {
+ ref T sourceRef = ref MemoryMarshal.GetReference(source);
+ ref float destinationRef = ref MemoryMarshal.GetReference(destination);
+ nuint length = (uint)source.Length;
+ nuint index = 0;
+
+ if (Vector512.IsHardwareAccelerated)
+ {
+ Vector512 divisor = Vector512.Create(TOperator.Divisor);
+ nuint count = (uint)Vector512.Count;
+
+ while (length - index >= count)
+ {
+ ref float destinationStart = ref Unsafe.Add(ref destinationRef, index);
+ TOperator.Invoke(Vector512.LoadUnsafe(ref sourceRef, index), divisor, ref destinationStart);
+ index += count;
+ }
+ }
+
+ if (Vector256.IsHardwareAccelerated)
+ {
+ Vector256 divisor = Vector256.Create(TOperator.Divisor);
+ nuint count = (uint)Vector256.Count;
+
+ while (length - index >= count)
+ {
+ ref float destinationStart = ref Unsafe.Add(ref destinationRef, index);
+ TOperator.Invoke(Vector256.LoadUnsafe(ref sourceRef, index), divisor, ref destinationStart);
+ index += count;
+ }
+ }
+
+ if (Vector128.IsHardwareAccelerated)
+ {
+ Vector128 divisor = Vector128.Create(TOperator.Divisor);
+ nuint count = (uint)Vector128.Count;
+
+ while (length - index >= count)
+ {
+ ref float destinationStart = ref Unsafe.Add(ref destinationRef, index);
+ TOperator.Invoke(Vector128.LoadUnsafe(ref sourceRef, index), divisor, ref destinationStart);
+ index += count;
+ }
+ }
+
+ // Preserve the scalar division expression for the final partial vector. Multiplication by a reciprocal
+ // is not bit-equivalent for every input and would change the values stored in the ICC profile model.
+ while (index < length)
+ {
+ Unsafe.Add(ref destinationRef, index) = TOperator.Invoke(Unsafe.Add(ref sourceRef, index));
+ index++;
+ }
+ }
+
+ ///
+ /// Converts packed byte entries to normalized single-precision values.
+ ///
+ private readonly struct ByteNormalizeOperator : INormalizeOperator
+ {
+ ///
+ public static float Divisor => byte.MaxValue;
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float Invoke(byte source)
+ => source / (float)byte.MaxValue;
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Invoke(Vector128 source, Vector128 divisor, ref float destination)
+ {
+ // [b0..b15] becomes four ordered groups of four UInt32 values. Every widened value is at most
+ // 255, so reinterpreting UInt32 as Int32 before conversion preserves its numeric value.
+ (Vector128 lower16, Vector128 upper16) = Vector128.Widen(source);
+ (Vector128 values0, Vector128 values1) = Vector128.Widen(lower16);
+ (Vector128 values2, Vector128 values3) = Vector128.Widen(upper16);
+
+ (Vector128.ConvertToSingle(values0.AsInt32()) / divisor).StoreUnsafe(ref destination);
+ (Vector128.ConvertToSingle(values1.AsInt32()) / divisor).StoreUnsafe(ref destination, 4);
+ (Vector128.ConvertToSingle(values2.AsInt32()) / divisor).StoreUnsafe(ref destination, 8);
+ (Vector128.ConvertToSingle(values3.AsInt32()) / divisor).StoreUnsafe(ref destination, 12);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Invoke(Vector256 source, Vector256 divisor, ref float destination)
+ {
+ // [b0..b31] becomes four ordered groups of eight UInt32 values, matching four contiguous
+ // Vector256 stores without shuffling the converted results.
+ (Vector256 lower16, Vector256 upper16) = Vector256.Widen(source);
+ (Vector256 values0, Vector256 values1) = Vector256.Widen(lower16);
+ (Vector256 values2, Vector256 values3) = Vector256.Widen(upper16);
+
+ (Vector256.ConvertToSingle(values0.AsInt32()) / divisor).StoreUnsafe(ref destination);
+ (Vector256.ConvertToSingle(values1.AsInt32()) / divisor).StoreUnsafe(ref destination, 8);
+ (Vector256.ConvertToSingle(values2.AsInt32()) / divisor).StoreUnsafe(ref destination, 16);
+ (Vector256.ConvertToSingle(values3.AsInt32()) / divisor).StoreUnsafe(ref destination, 24);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Invoke(Vector512 source, Vector512 divisor, ref float destination)
+ {
+ // [b0..b63] becomes four ordered groups of sixteen UInt32 values, matching four contiguous
+ // Vector512 stores. The portable widening APIs map to zero-extension instructions.
+ (Vector512 lower16, Vector512 upper16) = Vector512.Widen(source);
+ (Vector512 values0, Vector512 values1) = Vector512.Widen(lower16);
+ (Vector512 values2, Vector512 values3) = Vector512.Widen(upper16);
+
+ (Vector512.ConvertToSingle(values0.AsInt32()) / divisor).StoreUnsafe(ref destination);
+ (Vector512.ConvertToSingle(values1.AsInt32()) / divisor).StoreUnsafe(ref destination, 16);
+ (Vector512.ConvertToSingle(values2.AsInt32()) / divisor).StoreUnsafe(ref destination, 32);
+ (Vector512.ConvertToSingle(values3.AsInt32()) / divisor).StoreUnsafe(ref destination, 48);
+ }
+ }
+
+ ///
+ /// Converts packed unsigned-short entries to normalized single-precision values.
+ ///
+ private readonly struct UInt16NormalizeOperator : INormalizeOperator
+ {
+ ///
+ public static float Divisor => ushort.MaxValue;
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float Invoke(ushort source)
+ => source / (float)ushort.MaxValue;
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Invoke(Vector128 source, Vector128 divisor, ref float destination)
+ {
+ // [u0..u7] becomes two ordered groups of four UInt32 values. Every value is at most 65535,
+ // so signed conversion after reinterpretation is numerically identical to unsigned conversion.
+ (Vector128 lower, Vector128 upper) = Vector128.Widen(source);
+
+ (Vector128.ConvertToSingle(lower.AsInt32()) / divisor).StoreUnsafe(ref destination);
+ (Vector128.ConvertToSingle(upper.AsInt32()) / divisor).StoreUnsafe(ref destination, 4);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Invoke(Vector256 source, Vector256 divisor, ref float destination)
+ {
+ // [u0..u15] becomes two ordered groups of eight UInt32 values, matching two contiguous
+ // Vector256 stores without a result shuffle.
+ (Vector256 lower, Vector256 upper) = Vector256.Widen(source);
+
+ (Vector256.ConvertToSingle(lower.AsInt32()) / divisor).StoreUnsafe(ref destination);
+ (Vector256.ConvertToSingle(upper.AsInt32()) / divisor).StoreUnsafe(ref destination, 8);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Invoke(Vector512 source, Vector512 divisor, ref float destination)
+ {
+ // [u0..u31] becomes two ordered groups of sixteen UInt32 values, matching two contiguous
+ // Vector512 stores. The portable widening APIs map to zero-extension instructions.
+ (Vector512 lower, Vector512 upper) = Vector512.Widen(source);
+
+ (Vector512.ConvertToSingle(lower.AsInt32()) / divisor).StoreUnsafe(ref destination);
+ (Vector512.ConvertToSingle(upper.AsInt32()) / divisor).StoreUnsafe(ref destination, 16);
+ }
+ }
+}
diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcRecordNumber.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcRecordNumber.cs
index 2d5fe6a09..bbbeb83e0 100644
--- a/src/ImageSharp/Metadata/Profiles/IPTC/IptcRecordNumber.cs
+++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcRecordNumber.cs
@@ -9,12 +9,12 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.IPTC;
internal enum IptcRecordNumber : byte
{
///
- /// A Envelope Record.
+ /// An Envelope Record.
///
Envelope = 0x01,
///
- /// A Application Record.
+ /// An Application Record.
///
Application = 0x02
}
diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs
index 7735810b3..65daf5936 100644
--- a/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs
+++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Diagnostics;
+using System.Globalization;
using System.Text;
namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc;
@@ -9,7 +10,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc;
///
/// Represents a single value of the IPTC profile.
///
-[DebuggerDisplay("{Tag} = {ToString(),nq} ({GetType().Name,nq})")]
+[DebuggerDisplay("{Tag} = {DebuggerDisplayValue(),nq} ({GetType().Name,nq})")]
public sealed class IptcValue : IDeepCloneable
{
private byte[] data = [];
@@ -213,4 +214,37 @@ public sealed class IptcValue : IDeepCloneable
return encoding.GetString(this.data);
}
+
+ private string DebuggerDisplayValue()
+ {
+ // IPTC RecordVersion (2:00) is a 2-byte binary value, commonly 0x0004.
+ // Showing it as UTF-8 produces control characters like "\0\u0004".
+ if (this.Tag == IptcTag.RecordVersion && this.data.Length == 2)
+ {
+ int version = (this.data[0] << 8) | this.data[1];
+ return version.ToString(CultureInfo.InvariantCulture);
+ }
+
+ // Prefer readable text if it looks like it, otherwise show hex.
+ // (Avoid surprising debugger output for binary payloads.)
+ bool printable = true;
+ for (int i = 0; i < this.data.Length; i++)
+ {
+ byte b = this.data[i];
+
+ // If any byte is an ASCII control character, treat this value as binary.
+ if (b is < 0x20 or 0x7F)
+ {
+ printable = false;
+ break;
+ }
+ }
+
+ if (printable)
+ {
+ return this.Value;
+ }
+
+ return Convert.ToHexString(this.data);
+ }
}
diff --git a/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs b/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs
index 77ff35df0..639f09722 100644
--- a/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs
+++ b/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs
@@ -1,8 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.Diagnostics;
using System.Text;
+using System.Xml;
using System.Xml.Linq;
namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp;
@@ -25,18 +25,17 @@ public sealed class XmpProfile : IDeepCloneable
/// Initializes a new instance of the class.
///
/// The UTF8 encoded byte array to read the XMP profile from.
- public XmpProfile(byte[]? data) => this.Data = data;
+ public XmpProfile(byte[]? data) => this.Data = NormalizeDataIfNeeded(data);
///
- /// Initializes a new instance of the class
- /// by making a copy from another XMP profile.
+ /// Initializes a new instance of the class from an XML document.
+ /// The document is serialized as UTF-8 without BOM.
///
- /// The other XMP profile, from which the clone should be made from.
- private XmpProfile(XmpProfile other)
+ /// The XMP XML document.
+ public XmpProfile(XDocument document)
{
- Guard.NotNull(other, nameof(other));
-
- this.Data = other.Data;
+ Guard.NotNull(document, nameof(document));
+ this.Data = SerializeDocument(document);
}
///
@@ -45,30 +44,28 @@ public sealed class XmpProfile : IDeepCloneable
internal byte[]? Data { get; private set; }
///
- /// Gets the raw XML document containing the XMP profile.
+ /// Convert the content of this into an .
///
- /// The
- public XDocument? GetDocument()
+ /// The instance, or if no XMP data is present.
+ public XDocument? ToXDocument()
{
- byte[]? byteArray = this.Data;
- if (byteArray is null)
+ byte[]? data = this.Data;
+ if (data is null || data.Length == 0)
{
return null;
}
- // Strip leading whitespace, as the XmlReader doesn't like them.
- int count = byteArray.Length;
- for (int i = count - 1; i > 0; i--)
+ using MemoryStream stream = new(data, writable: false);
+
+ XmlReaderSettings settings = new()
{
- if (byteArray[i] is 0 or 0x0f)
- {
- count--;
- }
- }
+ DtdProcessing = DtdProcessing.Ignore,
+ XmlResolver = null,
+ CloseInput = false
+ };
- using MemoryStream stream = new(byteArray, 0, count);
- using StreamReader reader = new(stream, Encoding.UTF8);
- return XDocument.Load(reader);
+ using XmlReader reader = XmlReader.Create(stream, settings);
+ return XDocument.Load(reader, LoadOptions.PreserveWhitespace);
}
///
@@ -77,12 +74,101 @@ public sealed class XmpProfile : IDeepCloneable
/// The
public byte[] ToByteArray()
{
- Guard.NotNull(this.Data);
- byte[] result = new byte[this.Data.Length];
+ byte[]? data = this.Data;
+
+ if (data is null)
+ {
+ return [];
+ }
+
+ byte[] result = new byte[data.Length];
this.Data.AsSpan().CopyTo(result);
return result;
}
///
- public XmpProfile DeepClone() => new(this);
+ public XmpProfile DeepClone()
+ {
+ byte[]? data = this.Data;
+ if (data is null)
+ {
+ // Preserve the semantics of an "empty" profile when cloning.
+ return new XmpProfile();
+ }
+
+ byte[] clone = new byte[data.Length];
+ data.AsSpan().CopyTo(clone);
+ return new XmpProfile(clone);
+ }
+
+ private static byte[] SerializeDocument(XDocument document)
+ {
+ using MemoryStream ms = new();
+
+ XmlWriterSettings writerSettings = new()
+ {
+ Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), // no BOM
+ OmitXmlDeclaration = true, // generally safer for XMP consumers
+ Indent = false,
+ NewLineHandling = NewLineHandling.None
+ };
+
+ using (XmlWriter xw = XmlWriter.Create(ms, writerSettings))
+ {
+ document.Save(xw);
+ }
+
+ return ms.ToArray();
+ }
+
+ private static byte[]? NormalizeDataIfNeeded(byte[]? data)
+ {
+ if (data is null || data.Length == 0)
+ {
+ return data;
+ }
+
+ // Allocation-free fast path for the normal case.
+
+ // Check for UTF-8 BOM (0xEF,0xBB,0xBF)
+ bool hasBom = data.Length >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF;
+
+ // XMP metadata is commonly stored in fixed-size container blocks (e.g. TIFF tag 700).
+ // Producers often pad unused space so the packet can be updated in-place without
+ // rewriting the file. In practice this padding is either NUL (0x00) from the container
+ // or 0x0F used by Adobe XMP writers. Both are invalid XML and must be trimmed.
+ bool hasTrailingPad = data[^1] is 0 or 0x0F;
+
+ if (!hasBom && !hasTrailingPad)
+ {
+ return data;
+ }
+
+ int start = hasBom ? 3 : 0;
+ int end = data.Length;
+
+ if (hasTrailingPad)
+ {
+ while (end > start)
+ {
+ byte b = data[end - 1];
+ if (b is not 0 and not 0x0F)
+ {
+ break;
+ }
+
+ end--;
+ }
+ }
+
+ int length = end - start;
+ if (length <= 0)
+ {
+ return null;
+ }
+
+ byte[] normalized = new byte[length];
+ Buffer.BlockCopy(data, start, normalized, 0, length);
+ return normalized;
+ }
}
diff --git a/src/ImageSharp/PixelFormats/AssociatedAlphaPixelOperations{TPixel}.cs b/src/ImageSharp/PixelFormats/AssociatedAlphaPixelOperations{TPixel}.cs
new file mode 100644
index 000000000..ad26eb4e6
--- /dev/null
+++ b/src/ImageSharp/PixelFormats/AssociatedAlphaPixelOperations{TPixel}.cs
@@ -0,0 +1,387 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Numerics;
+using SixLabors.ImageSharp.ColorProfiles.Companding;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.PixelFormats.PixelBlenders;
+
+namespace SixLabors.ImageSharp.PixelFormats;
+
+///
+/// Provides bulk operations for pixel formats that store associated alpha.
+///
+/// The associated-alpha pixel format.
+public abstract class AssociatedAlphaPixelOperations : PixelOperations
+ where TPixel : unmanaged, IPixel
+{
+ ///
+ public override PixelBlender GetPixelBlender(PixelColorBlendingMode colorMode, PixelAlphaCompositionMode alphaMode)
+ => AssociatedAlphaPixelBlenders.GetPixelBlender(colorMode, alphaMode);
+
+ ///
+ protected abstract override void ToUnassociatedVector4(
+ Configuration configuration,
+ ReadOnlySpan source,
+ Span destination);
+
+ ///
+ protected abstract override void ToAssociatedVector4(
+ Configuration configuration,
+ ReadOnlySpan source,
+ Span destination);
+
+ ///
+ protected abstract override void FromUnassociatedVector4Destructive(
+ Configuration configuration,
+ Span source,
+ Span destination);
+
+ ///
+ protected abstract override void FromAssociatedVector4Destructive(
+ Configuration configuration,
+ Span source,
+ Span destination);
+
+ ///
+ protected abstract override void ToUnassociatedScaledVector4(
+ Configuration configuration,
+ ReadOnlySpan source,
+ Span destination);
+
+ ///
+ protected abstract override void ToAssociatedScaledVector4(
+ Configuration configuration,
+ ReadOnlySpan source,
+ Span destination);
+
+ ///
+ protected abstract override void FromUnassociatedScaledVector4Destructive(
+ Configuration configuration,
+ Span source,
+ Span destination);
+
+ ///
+ protected abstract override void FromAssociatedScaledVector4Destructive(Configuration configuration, Span source, Span destination);
+
+ ///
+ public override void From(
+ Configuration configuration,
+ ReadOnlySpan source,
+ Span destination)
+ {
+ if (source.IsEmpty)
+ {
+ return;
+ }
+
+ // Cap large conversions at 1,024 vectors while avoiding a 16 KiB rental for short spans.
+ int sliceLength = Math.Min(source.Length, 1024);
+ int numberOfSlices = source.Length / sliceLength;
+
+ using IMemoryOwner tempVectors = configuration.MemoryAllocator.Allocate(sliceLength);
+ Span vectorSpan = tempVectors.GetSpan()[..sliceLength];
+
+ // Convert through unassociated vectors so the destination operation can quantize alpha to its own storage before associating RGB.
+ for (int i = 0; i < numberOfSlices; i++)
+ {
+ int start = i * sliceLength;
+ ReadOnlySpan sourceSlice = source.Slice(start, sliceLength);
+ Span destinationSlice = destination.Slice(start, sliceLength);
+ PixelOperations.Instance.ToVector4(
+ configuration,
+ sourceSlice,
+ vectorSpan,
+ PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply);
+
+ this.FromUnassociatedScaledVector4Destructive(configuration, vectorSpan, destinationSlice);
+ }
+
+ int endOfCompleteSlices = numberOfSlices * sliceLength;
+ int remainder = source.Length - endOfCompleteSlices;
+
+ if (remainder > 0)
+ {
+ ReadOnlySpan sourceSlice = source[endOfCompleteSlices..];
+ Span destinationSlice = destination.Slice(endOfCompleteSlices, remainder);
+ vectorSpan = vectorSpan[..remainder];
+ PixelOperations.Instance.ToVector4(
+ configuration,
+ sourceSlice,
+ vectorSpan,
+ PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply);
+
+ this.FromUnassociatedScaledVector4Destructive(configuration, vectorSpan, destinationSlice);
+ }
+ }
+
+ ///
+ public override void FromVector4Destructive(
+ Configuration configuration,
+ Span sourceVectors,
+ Span destination,
+ PixelConversionModifiers modifiers)
+ {
+ Guard.NotNull(configuration, nameof(configuration));
+ Guard.DestinationShouldNotBeTooShort(sourceVectors, destination, nameof(destination));
+
+ bool associated = modifiers.IsDefined(PixelConversionModifiers.Premultiply) || !modifiers.IsDefined(PixelConversionModifiers.UnPremultiply);
+ bool scaled = modifiers.IsDefined(PixelConversionModifiers.Scale);
+
+ if (modifiers.IsDefined(PixelConversionModifiers.SRgbCompand))
+ {
+ // Transfer functions operate on straight color components. Associated input must therefore be unassociated before companding.
+ if (associated)
+ {
+ Numerics.UnPremultiply(sourceVectors);
+ }
+
+ SRgbCompanding.Compress(sourceVectors);
+
+ if (scaled)
+ {
+ this.FromUnassociatedScaledVector4Destructive(configuration, sourceVectors, destination);
+ }
+ else
+ {
+ this.FromUnassociatedVector4Destructive(configuration, sourceVectors, destination);
+ }
+
+ return;
+ }
+
+ if (scaled)
+ {
+ if (associated)
+ {
+ this.FromAssociatedScaledVector4Destructive(configuration, sourceVectors, destination);
+ }
+ else
+ {
+ this.FromUnassociatedScaledVector4Destructive(configuration, sourceVectors, destination);
+ }
+ }
+ else if (associated)
+ {
+ this.FromAssociatedVector4Destructive(configuration, sourceVectors, destination);
+ }
+ else
+ {
+ this.FromUnassociatedVector4Destructive(configuration, sourceVectors, destination);
+ }
+ }
+
+ ///
+ public override void ToVector4(
+ Configuration configuration,
+ ReadOnlySpan source,
+ Span destinationVectors,
+ PixelConversionModifiers modifiers)
+ {
+ Guard.NotNull(configuration, nameof(configuration));
+ Guard.DestinationShouldNotBeTooShort(source, destinationVectors, nameof(destinationVectors));
+
+ bool associated = modifiers.IsDefined(PixelConversionModifiers.Premultiply) || !modifiers.IsDefined(PixelConversionModifiers.UnPremultiply);
+ bool scaled = modifiers.IsDefined(PixelConversionModifiers.Scale);
+
+ if (modifiers.IsDefined(PixelConversionModifiers.SRgbCompand))
+ {
+ // Extract straight color before applying the transfer function; companding associated components would make RGB depend on alpha.
+ if (scaled)
+ {
+ this.ToUnassociatedScaledVector4(configuration, source, destinationVectors);
+ }
+ else
+ {
+ this.ToUnassociatedVector4(configuration, source, destinationVectors);
+ }
+
+ Span converted = destinationVectors[..source.Length];
+ SRgbCompanding.Expand(converted);
+
+ if (associated)
+ {
+ Numerics.Premultiply(converted);
+ }
+
+ return;
+ }
+
+ if (scaled)
+ {
+ if (associated)
+ {
+ this.ToAssociatedScaledVector4(configuration, source, destinationVectors);
+ }
+ else
+ {
+ this.ToUnassociatedScaledVector4(configuration, source, destinationVectors);
+ }
+ }
+ else if (associated)
+ {
+ this.ToAssociatedVector4(configuration, source, destinationVectors);
+ }
+ else
+ {
+ this.ToUnassociatedVector4(configuration, source, destinationVectors);
+ }
+ }
+
+ ///
+ public override void FromArgb32(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.From(configuration, source, destination);
+
+ ///
+ public override void FromAbgr32(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.From(configuration, source, destination);
+
+ ///
+ public override void FromBgr24(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.From(configuration, source, destination);
+
+ ///
+ public override void FromBgra32(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.From(configuration, source, destination);
+
+ ///
+ public override void FromL8(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.From(configuration, source, destination);
+
+ ///
+ public override void FromL16(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.From(configuration, source, destination);
+
+ ///
+ public override void FromLa16(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.From(configuration, source, destination);
+
+ ///
+ public override void FromLa32(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.From(configuration, source, destination);
+
+ ///
+ public override void FromRgb24(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.From(configuration, source, destination);
+
+ ///
+ public override void FromRgba32(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.From(configuration, source, destination);
+
+ ///
+ public override void FromRgb48(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.From(configuration, source, destination);
+
+ ///
+ public override void FromRgba64(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.From(configuration, source, destination);
+
+ ///
+ public override void FromBgra5551(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.From(configuration, source, destination);
+
+ ///
+ public override void ToArgb32(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.ConvertToUnassociated(configuration, source, destination);
+
+ ///
+ public override void ToAbgr32(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.ConvertToUnassociated(configuration, source, destination);
+
+ ///
+ public override void ToBgr24(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.ConvertToUnassociated(configuration, source, destination);
+
+ ///
+ public override void ToBgra32(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.ConvertToUnassociated(configuration, source, destination);
+
+ ///
+ public override void ToL8(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.ConvertToUnassociated(configuration, source, destination);
+
+ ///
+ public override void ToL16(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.ConvertToUnassociated(configuration, source, destination);
+
+ ///
+ public override void ToLa16(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.ConvertToUnassociated(configuration, source, destination);
+
+ ///
+ public override void ToLa32(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.ConvertToUnassociated(configuration, source, destination);
+
+ ///
+ public override void ToRgb24(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.ConvertToUnassociated(configuration, source, destination);
+
+ ///
+ public override void ToRgba32(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.ConvertToUnassociated(configuration, source, destination);
+
+ ///
+ public override void ToRgb48(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.ConvertToUnassociated(configuration, source, destination);
+
+ ///
+ public override void ToRgba64(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.ConvertToUnassociated(configuration, source, destination);
+
+ ///
+ public override void ToBgra5551(Configuration configuration, ReadOnlySpan source, Span destination)
+ => this.ConvertToUnassociated(configuration, source, destination);
+
+ ///
+ /// Converts associated source pixels to an unassociated destination format.
+ ///
+ /// The destination pixel format.
+ /// The configuration.
+ /// The source pixels.
+ /// The destination pixels.
+ private void ConvertToUnassociated(
+ Configuration configuration,
+ ReadOnlySpan source,
+ Span destination)
+ where TDestinationPixel : unmanaged, IPixel
+ {
+ Guard.NotNull(configuration, nameof(configuration));
+ Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
+
+ if (source.IsEmpty)
+ {
+ return;
+ }
+
+ int sliceLength = Math.Min(source.Length, 1024);
+ int numberOfSlices = source.Length / sliceLength;
+
+ using IMemoryOwner tempVectors = configuration.MemoryAllocator.Allocate(sliceLength);
+ Span vectorSpan = tempVectors.GetSpan()[..sliceLength];
+ PixelOperations destinationOperations = PixelOperations.Instance;
+
+ // Generated destination dispatch routes conversion back through this source operation's ToX override. Extract through this
+ // operation's protected bulk hook, then use the destination's public modifier contract to avoid recursive dispatch.
+ for (int i = 0; i < numberOfSlices; i++)
+ {
+ int start = i * sliceLength;
+ ReadOnlySpan sourceSlice = source.Slice(start, sliceLength);
+ Span destinationSlice = destination.Slice(start, sliceLength);
+ this.ToUnassociatedScaledVector4(configuration, sourceSlice, vectorSpan);
+ destinationOperations.FromVector4Destructive(configuration, vectorSpan, destinationSlice, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply);
+ }
+
+ int endOfCompleteSlices = numberOfSlices * sliceLength;
+ int remainder = source.Length - endOfCompleteSlices;
+
+ if (remainder > 0)
+ {
+ ReadOnlySpan sourceSlice = source[endOfCompleteSlices..];
+ Span destinationSlice = destination.Slice(endOfCompleteSlices, remainder);
+ vectorSpan = vectorSpan[..remainder];
+ this.ToUnassociatedScaledVector4(configuration, sourceSlice, vectorSpan);
+ destinationOperations.FromVector4Destructive(configuration, vectorSpan, destinationSlice, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply);
+ }
+ }
+}
diff --git a/src/ImageSharp/PixelFormats/HalfTypeHelper.cs b/src/ImageSharp/PixelFormats/HalfTypeHelper.cs
index 02936f160..53f22eb09 100644
--- a/src/ImageSharp/PixelFormats/HalfTypeHelper.cs
+++ b/src/ImageSharp/PixelFormats/HalfTypeHelper.cs
@@ -1,7 +1,9 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Numerics;
using System.Runtime.CompilerServices;
+using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.PixelFormats;
@@ -10,6 +12,27 @@ namespace SixLabors.ImageSharp.PixelFormats;
///
internal static class HalfTypeHelper
{
+ // IEEE 754 binary16 has a largest finite magnitude of 65504. Scaled pixel vectors map that complete finite
+ // interval to [0, 1], while native vectors continue to expose the stored floating-point value directly.
+ internal const float FiniteMinimum = -65504F;
+ internal const float FiniteMaximum = 65504F;
+ internal const float FiniteRange = FiniteMaximum - FiniteMinimum;
+ internal const float InverseFiniteRange = (float)(1D / FiniteRange);
+ internal const float ScaledMidpoint = .5F;
+
+ // These constants mirror the binary16 conversion used by System.Half. Keeping the vector conversion
+ // bit-for-bit equivalent to the scalar runtime conversion makes SIMD a pure throughput optimization.
+ private const uint HalfExponentMask = 0x7C00;
+ private const uint HalfSignMask = 0x8000;
+ private const uint HalfToSingleBitsMask = 0x0FFF_E000;
+ private const uint SingleExponentLowerBound = 0x3880_0000;
+ private const uint SingleExponentOffset = 0x3800_0000;
+ private const uint SingleExponent126 = 0x3F00_0000;
+ private const uint SingleBiasedExponentMask = 0x7F80_0000;
+ private const uint SingleExponent13 = 0x0680_0000;
+ private const uint SingleSignMask = 0x8000_0000;
+ private const float MaxHalfValueBelowInfinity = 65520F;
+
///
/// Packs a into an
///
@@ -25,4 +48,304 @@ internal static class HalfTypeHelper
/// The .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static float Unpack(ushort value) => (float)BitConverter.UInt16BitsToHalf(value);
+
+ ///
+ /// Normalizes a finite binary16 value to the scaled pixel range.
+ ///
+ /// The native binary16 value represented as a .
+ /// The normalized value.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static float ToScaled(float value) => (value * InverseFiniteRange) + ScaledMidpoint;
+
+ ///
+ /// Normalizes finite binary16 values to the scaled pixel range.
+ ///
+ /// The native binary16 values.
+ /// The normalized values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static Vector2 ToScaled(Vector2 value) => (value * InverseFiniteRange) + new Vector2(ScaledMidpoint);
+
+ ///
+ /// Normalizes finite binary16 values to the scaled pixel range.
+ ///
+ /// The native binary16 values.
+ /// The normalized values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static Vector4 ToScaled(Vector4 value) => (value * InverseFiniteRange) + new Vector4(ScaledMidpoint);
+
+ ///
+ /// Expands a normalized value to the finite binary16 range.
+ ///
+ /// The normalized value.
+ /// The native binary16 value represented as a .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static float FromScaled(float value) => (value * FiniteRange) + FiniteMinimum;
+
+ ///
+ /// Expands normalized values to the finite binary16 range.
+ ///
+ /// The normalized values.
+ /// The native binary16 values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static Vector2 FromScaled(Vector2 value) => (value * FiniteRange) + new Vector2(FiniteMinimum);
+
+ ///
+ /// Expands normalized values to the finite binary16 range.
+ ///
+ /// The normalized values.
+ /// The native binary16 values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static Vector4 FromScaled(Vector4 value) => (value * FiniteRange) + new Vector4(FiniteMinimum);
+
+ ///
+ /// Unpacks eight binary16 values into two vectors of single-precision values.
+ ///
+ /// The packed binary16 values.
+ /// The unpacked lower and upper values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static (Vector128 Lower, Vector128 Upper) Unpack(Vector128 value)
+ {
+ (Vector128 lower, Vector128 upper) = Vector128.Widen(value);
+ return (ConvertHalfBitsToSingle(lower), ConvertHalfBitsToSingle(upper));
+ }
+
+ ///
+ /// Unpacks sixteen binary16 values into two vectors of single-precision values.
+ ///
+ /// The packed binary16 values.
+ /// The unpacked lower and upper values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static (Vector256 Lower, Vector256 Upper) Unpack(Vector256 value)
+ {
+ (Vector256 lower, Vector256 upper) = Vector256.Widen(value);
+ return (ConvertHalfBitsToSingle(lower), ConvertHalfBitsToSingle(upper));
+ }
+
+ ///
+ /// Unpacks thirty-two binary16 values into two vectors of single-precision values.
+ ///
+ /// The packed binary16 values.
+ /// The unpacked lower and upper values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static (Vector512 Lower, Vector512 Upper) Unpack(Vector512 value)
+ {
+ (Vector512 lower, Vector512 upper) = Vector512.Widen(value);
+ return (ConvertHalfBitsToSingle(lower), ConvertHalfBitsToSingle(upper));
+ }
+
+ ///
+ /// Packs eight single-precision values into binary16 storage.
+ ///
+ /// The lower single-precision values.
+ /// The upper single-precision values.
+ /// The packed binary16 values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static Vector128 Pack(Vector128 lower, Vector128 upper)
+ => Vector128.Narrow(ConvertSingleToHalfBits(lower), ConvertSingleToHalfBits(upper));
+
+ ///
+ /// Packs sixteen single-precision values into binary16 storage.
+ ///
+ /// The lower single-precision values.
+ /// The upper single-precision values.
+ /// The packed binary16 values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static Vector256 Pack(Vector256 lower, Vector256 upper)
+ => Vector256.Narrow(ConvertSingleToHalfBits(lower), ConvertSingleToHalfBits(upper));
+
+ ///
+ /// Packs thirty-two single-precision values into binary16 storage.
+ ///
+ /// The lower single-precision values.
+ /// The upper single-precision values.
+ /// The packed binary16 values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static Vector512 Pack(Vector512 lower, Vector512 upper)
+ => Vector512.Narrow(ConvertSingleToHalfBits(lower), ConvertSingleToHalfBits(upper));
+
+ ///
+ /// Rounds single-precision values through binary16 without changing the vector width.
+ ///
+ /// The single-precision values.
+ /// The values after binary16 quantization.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static Vector128 RoundToHalf(Vector128 value)
+ => ConvertHalfBitsToSingle(ConvertSingleToHalfBits(value));
+
+ ///
+ /// Rounds single-precision values through binary16 without changing the vector width.
+ ///
+ /// The single-precision values.
+ /// The values after binary16 quantization.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static Vector256 RoundToHalf(Vector256 value)
+ => ConvertHalfBitsToSingle(ConvertSingleToHalfBits(value));
+
+ ///
+ /// Rounds single-precision values through binary16 without changing the vector width.
+ ///
+ /// The single-precision values.
+ /// The values after binary16 quantization.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static Vector512 RoundToHalf(Vector512 value)
+ => ConvertHalfBitsToSingle(ConvertSingleToHalfBits(value));
+
+ ///
+ /// Converts zero-extended binary16 bit patterns to single-precision values.
+ ///
+ /// The binary16 bit patterns.
+ /// The converted single-precision values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector128 ConvertHalfBitsToSingle(Vector128 value)
+ {
+ Vector128 sign = Vector128.ShiftLeft(value & Vector128.Create(HalfSignMask), 16);
+ Vector128 exponent = value & Vector128.Create(HalfExponentMask);
+ Vector128 subnormalMask = Vector128.Equals(exponent, Vector128.Zero);
+ Vector128 infinityOrNaNMask = Vector128.Equals(exponent, Vector128.Create(HalfExponentMask));
+ Vector128 maskedExponentLowerBound = subnormalMask & Vector128.Create(SingleExponentLowerBound);
+ Vector128 exponentOffset = Vector128.Create(SingleExponentOffset) | maskedExponentLowerBound;
+
+ // Binary16 and binary32 fraction fields differ by thirteen bits. Subnormals and special values
+ // need different exponent offsets before that shared field layout can be reinterpreted as float.
+ Vector128 bits = Vector128.ShiftLeft(value, 13) & Vector128.Create(HalfToSingleBitsMask);
+ exponentOffset = Vector128.ConditionalSelect(infinityOrNaNMask, Vector128.ShiftLeft(exponentOffset, 1), exponentOffset);
+ bits += exponentOffset;
+ Vector128 absoluteValue = (bits.AsSingle() - maskedExponentLowerBound.AsSingle()).AsUInt32();
+ return (absoluteValue | sign).AsSingle();
+ }
+
+ ///
+ /// Converts zero-extended binary16 bit patterns to single-precision values.
+ ///
+ /// The binary16 bit patterns.
+ /// The converted single-precision values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector256 ConvertHalfBitsToSingle(Vector256 value)
+ {
+ Vector256 sign = Vector256.ShiftLeft(value & Vector256.Create(HalfSignMask), 16);
+ Vector256 exponent = value & Vector256.Create(HalfExponentMask);
+ Vector256 subnormalMask = Vector256.Equals(exponent, Vector256.Zero);
+ Vector256 infinityOrNaNMask = Vector256.Equals(exponent, Vector256.Create(HalfExponentMask));
+ Vector256 maskedExponentLowerBound = subnormalMask & Vector256.Create(SingleExponentLowerBound);
+ Vector256 exponentOffset = Vector256.Create(SingleExponentOffset) | maskedExponentLowerBound;
+
+ // Binary16 and binary32 fraction fields differ by thirteen bits. Subnormals and special values
+ // need different exponent offsets before that shared field layout can be reinterpreted as float.
+ Vector256 bits = Vector256.ShiftLeft(value, 13) & Vector256.Create(HalfToSingleBitsMask);
+ exponentOffset = Vector256.ConditionalSelect(infinityOrNaNMask, Vector256.ShiftLeft(exponentOffset, 1), exponentOffset);
+ bits += exponentOffset;
+ Vector256 absoluteValue = (bits.AsSingle() - maskedExponentLowerBound.AsSingle()).AsUInt32();
+ return (absoluteValue | sign).AsSingle();
+ }
+
+ ///
+ /// Converts zero-extended binary16 bit patterns to single-precision values.
+ ///
+ /// The binary16 bit patterns.
+ /// The converted single-precision values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector512 ConvertHalfBitsToSingle(Vector512 value)
+ {
+ Vector512 sign = Vector512.ShiftLeft(value & Vector512.Create(HalfSignMask), 16);
+ Vector512 exponent = value & Vector512.Create(HalfExponentMask);
+ Vector512 subnormalMask = Vector512.Equals(exponent, Vector512.Zero);
+ Vector512 infinityOrNaNMask = Vector512.Equals(exponent, Vector512.Create(HalfExponentMask));
+ Vector512 maskedExponentLowerBound = subnormalMask & Vector512.Create(SingleExponentLowerBound);
+ Vector512 exponentOffset = Vector512.Create(SingleExponentOffset) | maskedExponentLowerBound;
+
+ // Binary16 and binary32 fraction fields differ by thirteen bits. Subnormals and special values
+ // need different exponent offsets before that shared field layout can be reinterpreted as float.
+ Vector512 bits = Vector512.ShiftLeft(value, 13) & Vector512.Create(HalfToSingleBitsMask);
+ exponentOffset = Vector512.ConditionalSelect(infinityOrNaNMask, Vector512.ShiftLeft(exponentOffset, 1), exponentOffset);
+ bits += exponentOffset;
+ Vector512 absoluteValue = (bits.AsSingle() - maskedExponentLowerBound.AsSingle()).AsUInt32();
+ return (absoluteValue | sign).AsSingle();
+ }
+
+ ///
+ /// Converts single-precision values to zero-extended binary16 bit patterns.
+ ///
+ /// The single-precision values.
+ /// The binary16 bit patterns in 32-bit lanes.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector128 ConvertSingleToHalfBits(Vector128 value)
+ {
+ Vector128 bits = value.AsUInt32();
+ Vector128 sign = Vector128.ShiftRightLogical(bits & Vector128.Create(SingleSignMask), 16);
+ Vector128 realMask = Vector128.Equals(value, value).AsUInt32();
+ value = Vector128.Abs(value);
+ value = Vector128.Min(Vector128.Create(MaxHalfValueBelowInfinity), value);
+ Vector128 exponentOffset = Vector128.Max(value, Vector128.Create(SingleExponentLowerBound).AsSingle()).AsUInt32();
+ exponentOffset &= Vector128.Create(SingleBiasedExponentMask);
+ exponentOffset += Vector128.Create(SingleExponent13);
+
+ // Adding an exponent-sized float rounds the significand to binary16 precision using IEEE
+ // round-to-nearest-even. The remaining integer operations realign the exponent and sign fields.
+ value += exponentOffset.AsSingle();
+ bits = value.AsUInt32() - Vector128.Create(SingleExponent126);
+ Vector128 newExponent = Vector128.ShiftRightLogical(bits, 13);
+ Vector128 maskedHalfExponentForNaN = ~realMask & Vector128.Create(HalfExponentMask);
+ bits &= realMask;
+ bits += newExponent;
+ bits &= ~maskedHalfExponentForNaN;
+ return bits | maskedHalfExponentForNaN | sign;
+ }
+
+ ///
+ /// Converts single-precision values to zero-extended binary16 bit patterns.
+ ///
+ /// The single-precision values.
+ /// The binary16 bit patterns in 32-bit lanes.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector256 ConvertSingleToHalfBits(Vector256 value)
+ {
+ Vector256 bits = value.AsUInt32();
+ Vector256 sign = Vector256.ShiftRightLogical(bits & Vector256.Create(SingleSignMask), 16);
+ Vector256 realMask = Vector256.Equals(value, value).AsUInt32();
+ value = Vector256.Abs(value);
+ value = Vector256.Min(Vector256.Create(MaxHalfValueBelowInfinity), value);
+ Vector256 exponentOffset = Vector256.Max(value, Vector256.Create(SingleExponentLowerBound).AsSingle()).AsUInt32();
+ exponentOffset &= Vector256.Create(SingleBiasedExponentMask);
+ exponentOffset += Vector256.Create(SingleExponent13);
+
+ // Adding an exponent-sized float rounds the significand to binary16 precision using IEEE
+ // round-to-nearest-even. The remaining integer operations realign the exponent and sign fields.
+ value += exponentOffset.AsSingle();
+ bits = value.AsUInt32() - Vector256.Create(SingleExponent126);
+ Vector256 newExponent = Vector256.ShiftRightLogical(bits, 13);
+ Vector256 maskedHalfExponentForNaN = ~realMask & Vector256.Create(HalfExponentMask);
+ bits &= realMask;
+ bits += newExponent;
+ bits &= ~maskedHalfExponentForNaN;
+ return bits | maskedHalfExponentForNaN | sign;
+ }
+
+ ///
+ /// Converts single-precision values to zero-extended binary16 bit patterns.
+ ///
+ /// The single-precision values.
+ /// The binary16 bit patterns in 32-bit lanes.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Vector512 ConvertSingleToHalfBits(Vector512 value)
+ {
+ Vector512 bits = value.AsUInt32();
+ Vector512 sign = Vector512.ShiftRightLogical(bits & Vector512.Create(SingleSignMask), 16);
+ Vector512 realMask = Vector512.Equals(value, value).AsUInt32();
+ value = Vector512.Abs(value);
+ value = Vector512.Min(Vector512.Create(MaxHalfValueBelowInfinity), value);
+ Vector512 exponentOffset = Vector512.Max(value, Vector512.Create(SingleExponentLowerBound).AsSingle()).AsUInt32();
+ exponentOffset &= Vector512.Create(SingleBiasedExponentMask);
+ exponentOffset += Vector512.Create(SingleExponent13);
+
+ // Adding an exponent-sized float rounds the significand to binary16 precision using IEEE
+ // round-to-nearest-even. The remaining integer operations realign the exponent and sign fields.
+ value += exponentOffset.AsSingle();
+ bits = value.AsUInt32() - Vector512.Create(SingleExponent126);
+ Vector512 newExponent = Vector512.ShiftRightLogical(bits, 13);
+ Vector512 maskedHalfExponentForNaN = ~realMask & Vector512.Create(HalfExponentMask);
+ bits &= realMask;
+ bits += newExponent;
+ bits &= ~maskedHalfExponentForNaN;
+ return bits | maskedHalfExponentForNaN | sign;
+ }
}
diff --git a/src/ImageSharp/PixelFormats/IPixel.cs b/src/ImageSharp/PixelFormats/IPixel.cs
index 528b3e76d..a4f6312dd 100644
--- a/src/ImageSharp/PixelFormats/IPixel.cs
+++ b/src/ImageSharp/PixelFormats/IPixel.cs
@@ -14,120 +14,150 @@ namespace SixLabors.ImageSharp.PixelFormats;
public interface IPixel : IPixel, IEquatable
where TSelf : unmanaged, IPixel
{
-#pragma warning disable CA1000 // Do not declare static members on generic types
///
/// Creates a instance for this pixel type.
/// This method is not intended to be consumed directly. Use instead.
///
/// The instance.
- static abstract PixelOperations CreatePixelOperations();
+ public static abstract PixelOperations CreatePixelOperations();
///
- /// Initializes the pixel instance from a generic a generic ("scaled") representation
- /// with values scaled and clamped between 0 and 1
+ /// Initializes the pixel instance from a generic ("scaled") representation using the pixel type's native alpha representation.
+ /// The scaled representation uses 0 and 1 as its nominal component bounds.
///
/// The vector to load the pixel from.
/// The .
- static abstract TSelf FromScaledVector4(Vector4 source);
+ public static abstract TSelf FromScaledVector4(Vector4 source);
///
- /// Initializes the pixel instance from a which is specific to the current pixel type.
+ /// Initializes the pixel instance from a generic ("scaled") whose color components use unassociated alpha.
+ /// The scaled representation uses 0 and 1 as its nominal component bounds.
///
/// The vector to load the pixel from.
/// The .
- static abstract TSelf FromVector4(Vector4 source);
+ public static abstract TSelf FromUnassociatedScaledVector4(Vector4 source);
+
+ ///
+ /// Initializes the pixel instance from a generic ("scaled") whose color components use associated alpha,
+ /// representing color multiplied by the logical opacity represented by alpha.
+ /// The scaled representation uses 0 and 1 as its nominal component bounds.
+ ///
+ /// The vector to load the pixel from.
+ /// The .
+ public static abstract TSelf FromAssociatedScaledVector4(Vector4 source);
+
+ ///
+ /// Initializes the pixel instance from a which is specific to the current pixel type and uses its native alpha representation.
+ ///
+ /// The vector to load the pixel from.
+ /// The .
+ public static abstract TSelf FromVector4(Vector4 source);
+
+ ///
+ /// Initializes the pixel instance from a which is specific to the current pixel type and whose color components use unassociated alpha.
+ ///
+ /// The vector to load the pixel from.
+ /// The .
+ public static abstract TSelf FromUnassociatedVector4(Vector4 source);
+
+ ///
+ /// Initializes the pixel instance from a which is specific to the current pixel type and whose color components use associated alpha,
+ /// representing color multiplied by the logical opacity represented by alpha.
+ ///
+ /// The vector to load the pixel from.
+ /// The .
+ public static abstract TSelf FromAssociatedVector4(Vector4 source);
///
/// Initializes the pixel instance from an value.
///
/// The value.
/// The .
- static abstract TSelf FromAbgr32(Abgr32 source);
+ public static abstract TSelf FromAbgr32(Abgr32 source);
///
/// Initializes the pixel instance from an value.
///
/// The value.
/// The .
- static abstract TSelf FromArgb32(Argb32 source);
+ public static abstract TSelf FromArgb32(Argb32 source);
///
/// Initializes the pixel instance from an value.
///
/// The value.
/// The .
- static abstract TSelf FromBgra5551(Bgra5551 source);
+ public static abstract TSelf FromBgra5551(Bgra5551 source);
///
/// Initializes the pixel instance from an value.
///
/// The value.
/// The .
- static abstract TSelf FromBgr24(Bgr24 source);
+ public static abstract TSelf FromBgr24(Bgr24 source);
///
/// Initializes the pixel instance from an value.
///
/// The value.
/// The .
- static abstract TSelf FromBgra32(Bgra32 source);
+ public static abstract TSelf FromBgra32(Bgra32 source);
///
/// Initializes the pixel instance from an value.
///
/// The value.
/// The .
- static abstract TSelf FromL8(L8 source);
+ public static abstract TSelf FromL8(L8 source);
///
/// Initializes the pixel instance from an value.
///
/// The value.
/// The .
- static abstract TSelf FromL16(L16 source);
+ public static abstract TSelf FromL16(L16 source);
///
/// Initializes the pixel instance from an value.
///
/// The value.
/// The .
- static abstract TSelf FromLa16(La16 source);
+ public static abstract TSelf FromLa16(La16 source);
///
/// Initializes the pixel instance from an value.
///
/// The value.
/// The .
- static abstract TSelf FromLa32(La32 source);
+ public static abstract TSelf FromLa32(La32 source);
///
/// Initializes the pixel instance from an value.
///
/// The value.
/// The .
- static abstract TSelf FromRgb24(Rgb24 source);
+ public static abstract TSelf FromRgb24(Rgb24 source);
///
/// Initializes the pixel instance from an value.
///
/// The value.
/// The .
- static abstract TSelf FromRgba32(Rgba32 source);
+ public static abstract TSelf FromRgba32(Rgba32 source);
///
/// Initializes the pixel instance from an value.
///
/// The value.
/// The .
- static abstract TSelf FromRgb48(Rgb48 source);
+ public static abstract TSelf FromRgb48(Rgb48 source);
///
/// Initializes the pixel instance from an value.
///
/// The value.
/// The .
- static abstract TSelf FromRgba64(Rgba64 source);
-#pragma warning restore CA1000 // Do not declare static members on generic types
+ public static abstract TSelf FromRgba64(Rgba64 source);
}
///
@@ -139,26 +169,58 @@ public interface IPixel
/// Gets the pixel type information.
///
/// The .
- static abstract PixelTypeInfo GetPixelTypeInfo();
+ public static abstract PixelTypeInfo GetPixelTypeInfo();
///
/// Convert the pixel instance into representation.
///
/// The
- Rgba32 ToRgba32();
+ public Rgba32 ToRgba32();
///
- /// Expands the pixel into a generic ("scaled") representation
- /// with values scaled and clamped between 0 and 1.
+ /// Expands the pixel into a generic ("scaled") representation using the pixel type's native alpha representation.
+ /// The scaled representation uses 0 and 1 as its nominal component bounds.
/// The vector components are typically expanded in least to greatest significance order.
///
/// The .
- Vector4 ToScaledVector4();
+ public Vector4 ToScaledVector4();
+
+ ///
+ /// Expands the pixel into a generic ("scaled") whose color components use unassociated alpha.
+ /// The scaled representation uses 0 and 1 as its nominal component bounds.
+ /// When alpha is zero and the pixel's native representation is associated, the color components remain unchanged
+ /// because no unassociated value can be recovered.
+ ///
+ /// The .
+ public Vector4 ToUnassociatedScaledVector4();
+
+ ///
+ /// Expands the pixel into a generic ("scaled") whose color components use associated alpha,
+ /// representing color multiplied by the logical opacity represented by alpha.
+ /// The scaled representation uses 0 and 1 as its nominal component bounds.
+ ///
+ /// The .
+ public Vector4 ToAssociatedScaledVector4();
///
- /// Expands the pixel into a which is specific to the current pixel type.
+ /// Expands the pixel into a which is specific to the current pixel type and uses its native alpha representation.
/// The vector components are typically expanded in least to greatest significance order.
///
/// The .
- Vector4 ToVector4();
+ public Vector4 ToVector4();
+
+ ///
+ /// Expands the pixel into a which is specific to the current pixel type and whose color components use unassociated alpha.
+ /// When alpha is zero and the pixel's native representation is associated, the color components remain unchanged
+ /// because no unassociated value can be recovered.
+ ///
+ /// The .
+ public Vector4 ToUnassociatedVector4();
+
+ ///
+ /// Expands the pixel into a which is specific to the current pixel type and whose color components use associated alpha,
+ /// representing color multiplied by the logical opacity represented by alpha.
+ ///
+ /// The .
+ public Vector4 ToAssociatedVector4();
}
diff --git a/src/ImageSharp/PixelFormats/PixelAlphaCompositionMode.cs b/src/ImageSharp/PixelFormats/PixelAlphaCompositionMode.cs
index f42a264db..b003e62a4 100644
--- a/src/ImageSharp/PixelFormats/PixelAlphaCompositionMode.cs
+++ b/src/ImageSharp/PixelFormats/PixelAlphaCompositionMode.cs
@@ -66,5 +66,10 @@ public enum PixelAlphaCompositionMode
///
/// Clear where they overlap.
///
- Xor
+ Xor,
+
+ ///
+ /// Adds the source and destination, clamping the result to the supported color and alpha range.
+ ///
+ Plus,
}
diff --git a/src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPixelBlenders.Generated.cs b/src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPixelBlenders.Generated.cs
new file mode 100644
index 000000000..b553e3412
--- /dev/null
+++ b/src/ImageSharp/PixelFormats/PixelBlenders/AssociatedAlphaPixelBlenders.Generated.cs
@@ -0,0 +1,6108 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+//
+using System.Numerics;
+using System.Runtime.Intrinsics;
+
+namespace SixLabors.ImageSharp.PixelFormats.PixelBlenders;
+
+///
+/// Provides the associated-alpha equations consumed by the shared pixel-blending traversal.
+///
+internal static class AssociatedAlphaPixelBlenderOperators
+{
+ ///
+ /// Applies the "NormalSrc" associated-alpha composition equation.
+ ///
+ public readonly struct NormalSrc : IPixelBlenderOperator
+ {
+ ///
+ public static bool IsAssociatedAlpha => true;
+
+ ///
+ public static Vector4 Invoke(Vector4 background, Vector4 source, float amount)
+ => AssociatedAlphaPorterDuffFunctions.NormalSrc(background, source, amount);
+
+ ///
+ public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount)
+ => AssociatedAlphaPorterDuffFunctions.NormalSrc(background, source, amount);
+
+ ///
+ public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount)
+ => AssociatedAlphaPorterDuffFunctions.NormalSrc(background, source, amount);
+ }
+
+ ///
+ /// Applies the "MultiplySrc" associated-alpha composition equation.
+ ///
+ public readonly struct MultiplySrc : IPixelBlenderOperator
+ {
+ ///
+ public static bool IsAssociatedAlpha => true;
+
+ ///
+ public static Vector4 Invoke(Vector4 background, Vector4 source, float amount)
+ => AssociatedAlphaPorterDuffFunctions.MultiplySrc(background, source, amount);
+
+ ///
+ public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount)
+ => AssociatedAlphaPorterDuffFunctions.MultiplySrc(background, source, amount);
+
+ ///
+ public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount)
+ => AssociatedAlphaPorterDuffFunctions.MultiplySrc(background, source, amount);
+ }
+
+ ///
+ /// Applies the "AddSrc" associated-alpha composition equation.
+ ///
+ public readonly struct AddSrc : IPixelBlenderOperator
+ {
+ ///
+ public static bool IsAssociatedAlpha => true;
+
+ ///
+ public static Vector4 Invoke(Vector4 background, Vector4 source, float amount)
+ => AssociatedAlphaPorterDuffFunctions.AddSrc(background, source, amount);
+
+ ///
+ public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount)
+ => AssociatedAlphaPorterDuffFunctions.AddSrc(background, source, amount);
+
+ ///
+ public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount)
+ => AssociatedAlphaPorterDuffFunctions.AddSrc(background, source, amount);
+ }
+
+ ///
+ /// Applies the "SubtractSrc" associated-alpha composition equation.
+ ///
+ public readonly struct SubtractSrc : IPixelBlenderOperator
+ {
+ ///
+ public static bool IsAssociatedAlpha => true;
+
+ ///
+ public static Vector4 Invoke(Vector4 background, Vector4 source, float amount)
+ => AssociatedAlphaPorterDuffFunctions.SubtractSrc(background, source, amount);
+
+ ///
+ public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount)
+ => AssociatedAlphaPorterDuffFunctions.SubtractSrc(background, source, amount);
+
+ ///
+ public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount)
+ => AssociatedAlphaPorterDuffFunctions.SubtractSrc(background, source, amount);
+ }
+
+ ///
+ /// Applies the "ScreenSrc" associated-alpha composition equation.
+ ///
+ public readonly struct ScreenSrc : IPixelBlenderOperator
+ {
+ ///
+ public static bool IsAssociatedAlpha => true;
+
+ ///
+ public static Vector4 Invoke(Vector4 background, Vector4 source, float amount)
+ => AssociatedAlphaPorterDuffFunctions.ScreenSrc(background, source, amount);
+
+ ///
+ public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount)
+ => AssociatedAlphaPorterDuffFunctions.ScreenSrc(background, source, amount);
+
+ ///
+ public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount)
+ => AssociatedAlphaPorterDuffFunctions.ScreenSrc(background, source, amount);
+ }
+
+ ///
+ /// Applies the "DarkenSrc" associated-alpha composition equation.
+ ///
+ public readonly struct DarkenSrc : IPixelBlenderOperator
+ {
+ ///
+ public static bool IsAssociatedAlpha => true;
+
+ ///
+ public static Vector4 Invoke(Vector4 background, Vector4 source, float amount)
+ => AssociatedAlphaPorterDuffFunctions.DarkenSrc(background, source, amount);
+
+ ///
+ public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount)
+ => AssociatedAlphaPorterDuffFunctions.DarkenSrc(background, source, amount);
+
+ ///
+ public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount)
+ => AssociatedAlphaPorterDuffFunctions.DarkenSrc(background, source, amount);
+ }
+
+ ///
+ /// Applies the "LightenSrc" associated-alpha composition equation.
+ ///
+ public readonly struct LightenSrc : IPixelBlenderOperator
+ {
+ ///
+ public static bool IsAssociatedAlpha => true;
+
+ ///
+ public static Vector4 Invoke(Vector4 background, Vector4 source, float amount)
+ => AssociatedAlphaPorterDuffFunctions.LightenSrc(background, source, amount);
+
+ ///
+ public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount)
+ => AssociatedAlphaPorterDuffFunctions.LightenSrc(background, source, amount);
+
+ ///
+ public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount)
+ => AssociatedAlphaPorterDuffFunctions.LightenSrc(background, source, amount);
+ }
+
+ ///
+ /// Applies the "OverlaySrc" associated-alpha composition equation.
+ ///
+ public readonly struct OverlaySrc : IPixelBlenderOperator
+ {
+ ///
+ public static bool IsAssociatedAlpha => true;
+
+ ///
+ public static Vector4 Invoke(Vector4 background, Vector4 source, float amount)
+ => AssociatedAlphaPorterDuffFunctions.OverlaySrc(background, source, amount);
+
+ ///
+ public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount)
+ => AssociatedAlphaPorterDuffFunctions.OverlaySrc(background, source, amount);
+
+ ///
+ public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount)
+ => AssociatedAlphaPorterDuffFunctions.OverlaySrc(background, source, amount);
+ }
+
+ ///
+ /// Applies the "HardLightSrc" associated-alpha composition equation.
+ ///
+ public readonly struct HardLightSrc : IPixelBlenderOperator
+ {
+ ///
+ public static bool IsAssociatedAlpha => true;
+
+ ///
+ public static Vector4 Invoke(Vector4 background, Vector4 source, float amount)
+ => AssociatedAlphaPorterDuffFunctions.HardLightSrc(background, source, amount);
+
+ ///
+ public static Vector256 Invoke(Vector256 background, Vector256 source, Vector256 amount)
+ => AssociatedAlphaPorterDuffFunctions.HardLightSrc(background, source, amount);
+
+ ///
+ public static Vector512 Invoke(Vector512 background, Vector512 source, Vector512 amount)
+ => AssociatedAlphaPorterDuffFunctions.HardLightSrc(background, source, amount);
+ }
+
+ ///
+ /// Applies the "ColorDodgeSrc" associated-alpha composition equation.
+ ///
+ public readonly struct ColorDodgeSrc : IPixelBlenderOperator
+ {
+ ///
+ public static bool IsAssociatedAlpha => true;
+
+ ///
+ public static Vector4 Invoke(Vector4 background, Vector4 source, float amount)
+ => AssociatedAlphaPorterDuffFunctions.ColorDodgeSrc(background, source, amount);
+
+ ///
+ public static Vector256 Invoke(Vector256 background, Vector256