diff --git a/.editorconfig b/.editorconfig
index 2e3045fb17..f579ff5d3d 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -104,8 +104,8 @@ dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:war
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning
dotnet_style_parentheses_in_other_operators = always_for_clarity:suggestion
# Expression-level preferences
-dotnet_style_object_initializer = true:warning
-dotnet_style_collection_initializer = true:warning
+dotnet_style_object_initializer = true:error
+dotnet_style_collection_initializer = true:error
dotnet_style_explicit_tuple_names = true:warning
dotnet_style_prefer_inferred_tuple_names = true:warning
dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
@@ -135,9 +135,9 @@ csharp_style_prefer_null_check_over_type_check = true:warning
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules#c-style-rules
[*.{cs,csx,cake}]
# 'var' preferences
-csharp_style_var_for_built_in_types = false:warning
-csharp_style_var_when_type_is_apparent = false:warning
-csharp_style_var_elsewhere = false:warning
+csharp_style_var_for_built_in_types = false:error
+csharp_style_var_when_type_is_apparent = false:error
+csharp_style_var_elsewhere = false:error
# Expression-bodied members
csharp_style_expression_bodied_methods = true:warning
csharp_style_expression_bodied_constructors = true:warning
@@ -160,7 +160,10 @@ csharp_style_pattern_local_over_anonymous_function = true:warning
csharp_style_deconstructed_variable_declaration = true:warning
csharp_style_prefer_index_operator = true:warning
csharp_style_prefer_range_operator = true:warning
-csharp_style_implicit_object_creation_when_type_is_apparent = true:warning
+csharp_style_implicit_object_creation_when_type_is_apparent = true:error
+# ReSharper inspection severities
+resharper_arrange_object_creation_when_type_evident_highlighting = error
+resharper_arrange_object_creation_when_type_not_evident_highlighting = error
# "Null" checking preferences
csharp_style_throw_expression = true:warning
csharp_style_conditional_delegate_call = true:warning
@@ -172,6 +175,11 @@ dotnet_diagnostic.IDE0063.severity = suggestion
csharp_using_directive_placement = outside_namespace:warning
# Modifier preferences
csharp_prefer_static_local_function = true:warning
+# Primary constructor preferences
+csharp_style_prefer_primary_constructors = false:none
+# Collection preferences
+dotnet_style_prefer_collection_expression = true:error
+resharper_use_collection_expression_highlighting =true:error
##########################################
# Unnecessary Code Rules
diff --git a/.gitattributes b/.gitattributes
index 3647a7063d..031420e539 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
###############################################################################
@@ -126,6 +123,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
@@ -133,3 +131,22 @@
*.pnm filter=lfs diff=lfs merge=lfs -text
*.wbmp filter=lfs diff=lfs merge=lfs -text
*.exr filter=lfs diff=lfs merge=lfs -text
+*.ico filter=lfs diff=lfs merge=lfs -text
+*.cur filter=lfs diff=lfs merge=lfs -text
+*.ani filter=lfs diff=lfs merge=lfs -text
+*.heic filter=lfs diff=lfs merge=lfs -text
+*.hif filter=lfs diff=lfs merge=lfs -text
+*.avif filter=lfs diff=lfs merge=lfs -text
+###############################################################################
+# 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 0000000000..5f9f694351
--- /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 e17b8d724e..634b904aee 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -11,64 +11,124 @@ 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
+ WarmLFS:
+ runs-on: ubuntu-latest
+ outputs:
+ lfs_key: ${{ steps.expose-key.outputs.lfs_key }}
+ steps:
+ - name: Git Config
+ shell: bash
+ run: |
+ git config --global core.autocrlf false
+ git config --global core.longpaths true
+
+ - name: Git Checkout
+ uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+ submodules: recursive
+
+ # Deterministic list of LFS object IDs, then compute a portable key:
+ # - `git lfs ls-files -l` lists all tracked LFS objects with their SHA-256
+ # - `awk '{print $1}'` extracts just the SHA field
+ # - `sort` sorts in byte order (hex hashes sort the same everywhere)
+ # This ensures the file content is identical regardless of OS or locale
+ - name: Git Create LFS id list
+ shell: bash
+ run: git lfs ls-files -l | awk '{print $1}' | sort > .lfs-assets-id
+
+ - name: Git Expose LFS cache key
+ id: expose-key
+ shell: bash
+ env:
+ LFS_KEY: lfs-${{ hashFiles('.lfs-assets-id') }}-v1
+ run: echo "lfs_key=$LFS_KEY" >> "$GITHUB_OUTPUT"
+
+ - name: Git Setup LFS Cache
+ uses: actions/cache@v6
+ with:
+ path: .git/lfs
+ key: ${{ steps.expose-key.outputs.lfs_key }}
+
+ - name: Git Pull LFS
+ shell: bash
+ run: git lfs pull
+
Build:
+ 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: net7.0
- sdk: 7.0.x
+ framework: net10.0
+ sdk: 10.0.x
sdk-preview: true
runtime: -x64
codecov: false
- - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable
- framework: net7.0
- sdk: 7.0.x
+ - os: macos-26
+ framework: net10.0
+ sdk: 10.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: windows-latest
- framework: net7.0
- sdk: 7.0.x
+ framework: net10.0
+ sdk: 10.0.x
sdk-preview: true
runtime: -x64
codecov: false
- - os: buildjet-4vcpu-ubuntu-2204-arm
- framework: net7.0
- sdk: 7.0.x
+ - os: ubuntu-22.04-arm
+ framework: net10.0
+ sdk: 10.0.x
sdk-preview: true
runtime: -x64
codecov: false
+
- os: ubuntu-latest
- framework: net6.0
- sdk: 6.0.x
+ framework: net8.0
+ sdk: 8.0.x
runtime: -x64
codecov: false
- - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable
- framework: net6.0
- sdk: 6.0.x
+ - os: macos-26
+ framework: net8.0
+ sdk: 8.0.x
runtime: -x64
codecov: false
- os: windows-latest
- framework: net6.0
- sdk: 6.0.x
+ framework: net8.0
+ sdk: 8.0.x
+ runtime: -x64
+ codecov: false
+ - os: ubuntu-22.04-arm
+ framework: net8.0
+ sdk: 8.0.x
runtime: -x64
codecov: false
- exclude:
- - isARM: false
- options:
- os: buildjet-4vcpu-ubuntu-2204-arm
- runs-on: ${{matrix.options.os}}
+ runs-on: ${{ matrix.options.os }}
steps:
- name: Install libgdi+, which is required for tests running on ubuntu
- if: ${{ matrix.options.os == 'buildjet-4vcpu-ubuntu-2204-arm' }}
- run: sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev
+ if: ${{ contains(matrix.options.os, 'ubuntu') }}
+ run: |
+ 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
@@ -77,30 +137,27 @@ jobs:
git config --global core.longpaths true
- name: Git Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: recursive
- # See https://github.com/actions/checkout/issues/165#issuecomment-657673315
- - name: Git Create LFS FileList
- run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id
-
+ # Use the warmed key from WarmLFS. Do not recompute or recreate .lfs-assets-id here.
- name: Git Setup LFS Cache
- uses: actions/cache@v3
- id: lfs-cache
+ uses: actions/cache@v6
with:
path: .git/lfs
- key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1
+ key: ${{ needs.WarmLFS.outputs.lfs_key }}
- name: Git Pull LFS
+ shell: bash
run: git lfs pull
- name: NuGet Install
- uses: NuGet/setup-nuget@v1
+ uses: NuGet/setup-nuget@v4
- name: NuGet Setup Cache
- uses: actions/cache@v3
+ uses: actions/cache@v6
id: nuget-cache
with:
path: ~/.nuget
@@ -109,17 +166,17 @@ jobs:
- name: DotNet Setup
if: ${{ matrix.options.sdk-preview != true }}
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@v6
with:
dotnet-version: |
- 6.0.x
+ 8.0.x
- name: DotNet Setup Preview
if: ${{ matrix.options.sdk-preview == true }}
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@v6
with:
dotnet-version: |
- 7.0.x
+ 10.0.x
- name: DotNet Build
if: ${{ matrix.options.sdk-preview != true }}
@@ -152,7 +209,7 @@ jobs:
XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit
- name: Export Failed Output
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v7
if: failure()
with:
name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip
@@ -160,11 +217,8 @@ jobs:
Publish:
needs: [Build]
-
runs-on: ubuntu-latest
-
if: (github.event_name == 'push')
-
steps:
- name: Git Config
shell: bash
@@ -173,16 +227,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@v1
+ uses: NuGet/setup-nuget@v4
- name: NuGet Setup Cache
- uses: actions/cache@v3
+ uses: actions/cache@v6
id: nuget-cache
with:
path: ~/.nuget
@@ -205,4 +259,3 @@ jobs:
run: |
dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json --skip-duplicate
dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json --skip-duplicate
-
diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml
index e551afbd6d..00f489b403 100644
--- a/.github/workflows/code-coverage.yml
+++ b/.github/workflows/code-coverage.yml
@@ -4,19 +4,26 @@ on:
schedule:
# 2AM every Tuesday/Thursday
- cron: "0 2 * * 2,4"
+
jobs:
Build:
strategy:
matrix:
options:
- os: ubuntu-latest
- framework: net6.0
+ framework: net8.0
runtime: -x64
codecov: true
- runs-on: ${{matrix.options.os}}
+ runs-on: ${{ matrix.options.os }}
steps:
+ - name: Install libgdi+, which is required for tests running on ubuntu
+ if: ${{ contains(matrix.options.os, 'ubuntu') }}
+ run: |
+ sudo apt-get update
+ sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev
+
- name: Git Config
shell: bash
run: |
@@ -24,30 +31,35 @@ jobs:
git config --global core.longpaths true
- name: Git Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: recursive
- # See https://github.com/actions/checkout/issues/165#issuecomment-657673315
- - name: Git Create LFS FileList
- run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id
+ # Deterministic list of LFS object IDs, then compute a portable key:
+ # - `git lfs ls-files -l` lists all tracked LFS objects with their SHA-256
+ # - `awk '{print $1}'` extracts just the SHA field
+ # - `sort` sorts in byte order (hex hashes sort the same everywhere)
+ # This ensures the file content is identical regardless of OS or locale
+ - name: Git Create LFS id list
+ shell: bash
+ run: git lfs ls-files -l | awk '{print $1}' | sort > .lfs-assets-id
- name: Git Setup LFS Cache
- uses: actions/cache@v3
+ uses: actions/cache@v6
id: lfs-cache
with:
path: .git/lfs
- key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1
+ key: lfs-${{ hashFiles('.lfs-assets-id') }}-v1
- name: Git Pull LFS
run: git lfs pull
- name: NuGet Install
- uses: NuGet/setup-nuget@v1
+ uses: NuGet/setup-nuget@v4
- name: NuGet Setup Cache
- uses: actions/cache@v3
+ uses: actions/cache@v6
id: nuget-cache
with:
path: ~/.nuget
@@ -55,33 +67,34 @@ jobs:
restore-keys: ${{ runner.os }}-nuget-
- name: DotNet Setup
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@v6
with:
dotnet-version: |
- 6.0.x
+ 8.0.x
- name: DotNet Build
shell: pwsh
- run: ./ci-build.ps1 "${{matrix.options.framework}}"
+ run: ./ci-build.ps1 "${{ matrix.options.framework }}"
env:
SIXLABORS_TESTING: True
- name: DotNet Test
shell: pwsh
- run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}"
+ run: ./ci-test.ps1 "${{ matrix.options.os }}" "${{ matrix.options.framework }}" "${{ matrix.options.runtime }}" "${{ matrix.options.codecov }}"
env:
SIXLABORS_TESTING: True
XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit
- name: Export Failed Output
- uses: actions/upload-artifact@v3
+ 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@v3
+ 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 fadf36964c..a8d2917be7 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 0000000000..814e3ce263
--- /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 0000000000..5f08a449a6
--- /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 26b3cc5afc..8219d5a293 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -21,9 +21,21 @@
-
-
- preview
+
+
+
+
+
+
+ 12.0
+
+
+
+ 14.0
@@ -22,21 +23,19 @@
-
- 3.1
+
+ 4.0
- net7.0;net6.0
- true
+ net8.0;net10.0
- net6.0
- true
+ net8.0
@@ -48,15 +47,24 @@
-
+
+
+
+
+
+ True
+ True
+ InlineArray.tt
+
+
True
True
- Block8x8F.Generated.tt
+ ImageExtensions.Save.tt
-
+
True
True
- Block8x8F.Generated.tt
+ ImageMetadataExtensions.tt
True
@@ -133,26 +141,31 @@
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
-
+
TextTemplatingFileGenerator
- Block8x8F.Generated.cs
+ InlineArray.cs
-
+
+ ImageMetadataExtensions.cs
TextTemplatingFileGenerator
- Block8x8F.Generated.cs
TextTemplatingFileGenerator
@@ -218,7 +231,15 @@
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 c24014e698..4881518c9c 100644
--- a/src/ImageSharp/Image{TPixel}.cs
+++ b/src/ImageSharp/Image{TPixel}.cs
@@ -2,9 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
-using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
@@ -78,7 +76,7 @@ public Image(int width, int height)
/// The height of the image in pixels.
/// The images metadata.
internal Image(Configuration configuration, int width, int height, ImageMetadata? metadata)
- : base(configuration, PixelTypeInfo.Create(), metadata ?? new(), width, height)
+ : base(configuration, TPixel.GetPixelTypeInfo(), metadata ?? new ImageMetadata(), width, height)
=> this.frames = new ImageFrameCollection(this, width, height, default(TPixel));
///
@@ -92,7 +90,7 @@ internal 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)
{
}
@@ -111,8 +109,29 @@ internal Image(
int width,
int height,
ImageMetadata metadata)
- : base(configuration, PixelTypeInfo.Create(), metadata, width, height)
- => this.frames = new ImageFrameCollection(this, width, height, memoryGroup);
+ : 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, rowStride, memoryGroup);
///
/// Initializes a new instance of the class
@@ -129,7 +148,7 @@ internal Image(
int height,
TPixel backgroundColor,
ImageMetadata? metadata)
- : base(configuration, PixelTypeInfo.Create(), metadata ?? new(), width, height)
+ : base(configuration, TPixel.GetPixelTypeInfo(), metadata ?? new ImageMetadata(), width, height)
=> this.frames = new ImageFrameCollection(this, width, height, backgroundColor);
///
@@ -140,7 +159,7 @@ internal Image(
/// The images metadata.
/// The frames that will be owned by this image instance.
internal Image(Configuration configuration, ImageMetadata metadata, IEnumerable> frames)
- : base(configuration, PixelTypeInfo.Create(), metadata, ValidateFramesAndGetSize(frames))
+ : base(configuration, TPixel.GetPixelTypeInfo(), metadata, ValidateFramesAndGetSize(frames))
=> this.frames = new ImageFrameCollection(this, frames);
///
@@ -161,7 +180,7 @@ internal Image(Configuration configuration, ImageMetadata metadata, IEnumerable<
///
/// Gets the root frame.
///
- private IPixelSource PixelSourceUnsafe => this.frames.RootFrameUnsafe;
+ private ImageFrame PixelSourceUnsafe => this.frames.RootFrameUnsafe;
///
/// Gets or sets the pixel at the specified position.
@@ -288,16 +307,24 @@ public void ProcessPixelRows(
}
///
- /// 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
@@ -312,20 +339,10 @@ public void ProcessPixelRows(
/// 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
+ /// Clones the current image.
///
/// Returns a new image with all the same metadata as the original.
public Image Clone() => this.Clone(this.Configuration);
@@ -396,22 +413,42 @@ internal override Task AcceptAsync(IImageVisitorAsync visitor, CancellationToken
}
///
- /// Switches the buffers used by the image and the pixelSource meaning that the Image will "own" the buffer from the pixelSource and the pixelSource will now own the Images buffer.
+ /// Switches the buffers used by the image and the pixel source meaning that the Image will
+ /// "own" the buffer from the pixelSource and the pixel source will now own the Image buffer.
///
- /// The pixel source.
- internal void SwapOrCopyPixelsBuffersFrom(Image pixelSource)
+ /// The pixel source.
+ internal void SwapOrCopyPixelsBuffersFrom(Image source)
{
- Guard.NotNull(pixelSource, nameof(pixelSource));
+ Guard.NotNull(source, nameof(source));
this.EnsureNotDisposed();
- ImageFrameCollection sourceFrames = pixelSource.Frames;
+ ImageFrameCollection sourceFrames = source.Frames;
for (int i = 0; i < this.frames.Count; i++)
{
this.frames[i].SwapOrCopyPixelsBufferFrom(sourceFrames[i]);
}
- this.UpdateSize(pixelSource.Size);
+ this.UpdateSize(source.Size);
+ }
+
+ ///
+ /// Copies the metadata from the source image.
+ ///
+ /// The metadata source.
+ internal void CopyMetadataFrom(Image source)
+ {
+ Guard.NotNull(source, nameof(source));
+
+ this.EnsureNotDisposed();
+
+ ImageFrameCollection sourceFrames = source.Frames;
+ for (int i = 0; i < this.frames.Count; i++)
+ {
+ this.frames[i].CopyMetadataFrom(sourceFrames[i]);
+ }
+
+ this.UpdateMetadata(source.Metadata);
}
private static Size ValidateFramesAndGetSize(IEnumerable> frames)
@@ -420,9 +457,9 @@ private static Size ValidateFramesAndGetSize(IEnumerable> fra
ImageFrame? rootFrame = frames.FirstOrDefault() ?? throw new ArgumentException("Must not be empty.", nameof(frames));
- Size rootSize = rootFrame.Size();
+ Size rootSize = rootFrame.Size;
- if (frames.Any(f => f.Size() != rootSize))
+ if (frames.Any(f => f.Size != rootSize))
{
throw new ArgumentException("The provided frames must be of the same size.", nameof(frames));
}
diff --git a/src/ImageSharp/IndexedImageFrame{TPixel}.cs b/src/ImageSharp/IndexedImageFrame{TPixel}.cs
index 6807e77ad2..a88cdb524e 100644
--- a/src/ImageSharp/IndexedImageFrame{TPixel}.cs
+++ b/src/ImageSharp/IndexedImageFrame{TPixel}.cs
@@ -25,12 +25,12 @@ public sealed class IndexedImageFrame : IPixelSource, IDisposable
/// Initializes a new instance of the class.
///
///
- /// The configuration which allows altering default behaviour or extending the library.
+ /// The configuration which allows altering default behavior or extending the library.
///
/// The frame width.
/// The frame height.
/// The color palette.
- internal IndexedImageFrame(Configuration configuration, int width, int height, ReadOnlyMemory palette)
+ public IndexedImageFrame(Configuration configuration, int width, int height, ReadOnlyMemory palette)
{
Guard.NotNull(configuration, nameof(configuration));
Guard.MustBeLessThanOrEqualTo(palette.Length, QuantizerConstants.MaxColors, nameof(palette));
@@ -42,14 +42,14 @@ internal IndexedImageFrame(Configuration configuration, int width, int height, R
this.Height = height;
this.pixelBuffer = configuration.MemoryAllocator.Allocate2D(width, height);
- // Copy the palette over. We want the lifetime of this frame to be independant of any palette source.
+ // Copy the palette over. We want the lifetime of this frame to be independent of any palette source.
this.paletteOwner = configuration.MemoryAllocator.Allocate(palette.Length);
palette.Span.CopyTo(this.paletteOwner.GetSpan());
this.Palette = this.paletteOwner.Memory[..palette.Length];
}
///
- /// Gets the configuration which allows altering default behaviour or extending the library.
+ /// Gets the configuration which allows altering default behavior or extending the library.
///
public Configuration Configuration { get; }
diff --git a/src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs b/src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs
new file mode 100644
index 0000000000..764bb37e82
--- /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 0000000000..1e9a632ed8
--- /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 3ead1c5df7..986ed7f7cf 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 9f34602fb1..c22e827a8a 100644
--- a/src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs
+++ b/src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs
@@ -47,7 +47,7 @@ public BasicArrayBuffer(T[] array)
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 a6ed797d6d..84dd065f54 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 4a202a96c3..b0fe0b6496 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 @@ protected RefCountedMemoryLifetimeGuard()
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 @@ private void ReleaseRef(bool finalizing)
}
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 f9434ee941..97fab1b680 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 @@ public SharedArrayPoolBuffer(int lengthInElements)
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)
{
@@ -57,13 +60,7 @@ public void AddRef()
[Conditional("DEBUG")]
[MemberNotNull(nameof(Array))]
- private void CheckDisposed()
- {
- if (this.Array == null)
- {
- throw new ObjectDisposedException("SharedArrayPoolBuffer");
- }
- }
+ private void CheckDisposed() => ObjectDisposedException.ThrowIf(this.Array == null, this.Array);
private sealed class LifetimeGuard : RefCountedMemoryLifetimeGuard
{
diff --git a/src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.LifetimeGuards.cs b/src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.LifetimeGuards.cs
index 24bf52b1f9..e3b73204de 100644
--- a/src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.LifetimeGuards.cs
+++ b/src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.LifetimeGuards.cs
@@ -11,7 +11,7 @@ public UnmanagedBuffer CreateGuardedBuffer(
bool clear)
where T : struct
{
- var buffer = new UnmanagedBuffer(lengthInElements, new ReturnToPoolBufferLifetimeGuard(this, handle));
+ UnmanagedBuffer buffer = new(lengthInElements, new ReturnToPoolBufferLifetimeGuard(this, handle));
if (clear)
{
buffer.Clear();
diff --git a/src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.cs b/src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.cs
index aa8bcd3859..d4ad22060d 100644
--- a/src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.cs
+++ b/src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.cs
@@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Memory.Internals;
internal partial class UniformUnmanagedMemoryPool : System.Runtime.ConstrainedExecution.CriticalFinalizerObject
{
private static int minTrimPeriodMilliseconds = int.MaxValue;
- private static readonly List> AllPools = new();
+ private static readonly List> AllPools = [];
private static Timer? trimTimer;
private static readonly Stopwatch Stopwatch = Stopwatch.StartNew();
@@ -337,6 +337,6 @@ public class TrimSettings
public bool Enabled => this.Rate > 0;
- public static TrimSettings Default => new TrimSettings();
+ public static TrimSettings Default => new();
}
}
diff --git a/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs
index de09647261..3a729cdf2b 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,11 +31,14 @@ public UnmanagedBuffer(int lengthInElements, UnmanagedBufferLifetimeGuard lifeti
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);
DebugGuard.NotDisposed(this.lifetimeGuard.IsDisposed, this.lifetimeGuard.GetType().Name);
- return new(this.Pointer, this.lengthInElements);
+ return new Span(this.Pointer, this.lengthInElements);
}
///
@@ -52,7 +55,7 @@ public override MemoryHandle Pin(int elementIndex = 0)
}
///
- 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 6b31cadf4f..632e1bec04 100644
--- a/src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs
+++ b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs
@@ -39,13 +39,13 @@ private UnmanagedMemoryHandle(IntPtr handle, int lengthInBytes)
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 @@ public void Free()
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 8eaf0b6d69..591c3b9dd4 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 static MemoryAllocator Create(MemoryAllocatorOptions options)
/// 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 @@ internal MemoryGroup AllocateGroup(
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 d9ba62c1ef..0578fd64d2 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 int? MaximumPoolSizeMegabytes
///
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 675afe8b9f..5d183fa442 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 621073a3db..cfffc679a2 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,32 +70,27 @@ internal UniformUnmanagedMemoryPoolMemoryAllocator(
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)
{
- var buffer = new SharedArrayPoolBuffer(length);
+ SharedArrayPoolBuffer buffer = new(length);
if (options.Has(AllocationOptions.Clean))
{
buffer.GetSpan().Clear();
@@ -105,17 +99,16 @@ public override IMemoryOwner Allocate(
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);
}
///
@@ -127,7 +120,7 @@ internal override MemoryGroup AllocateGroupCore(
{
if (totalLengthInBytes <= this.sharedArrayPoolThresholdInBytes)
{
- var buffer = new SharedArrayPoolBuffer((int)totalLengthInElements);
+ SharedArrayPoolBuffer buffer = new((int)totalLengthInElements);
return MemoryGroup.CreateContiguous(buffer, options.Has(AllocationOptions.Clean));
}
@@ -155,20 +148,14 @@ internal override MemoryGroup AllocateGroupCore(
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 da202aa596..eb52da7c03 100644
--- a/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs
+++ b/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs
@@ -18,9 +18,16 @@ 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
{
- var buffer = UnmanagedBuffer.Allocate(length);
+ UnmanagedBuffer buffer = UnmanagedBuffer.Allocate(length);
if (options.Has(AllocationOptions.Clean))
{
buffer.GetSpan().Clear();
diff --git a/src/ImageSharp/Memory/Buffer2DExtensions.cs b/src/ImageSharp/Memory/Buffer2DExtensions.cs
index 2eb05ea935..290d978d0b 100644
--- a/src/ImageSharp/Memory/Buffer2DExtensions.cs
+++ b/src/ImageSharp/Memory/Buffer2DExtensions.cs
@@ -25,27 +25,65 @@ public static IMemoryGroup GetMemoryGroup(this Buffer2D buffer)
return buffer.FastMemoryGroup.View;
}
+ ///
+ /// Performs a deep clone of the buffer covering the specified .
+ ///
+ /// The element type.
+ /// The source buffer.
+ /// The configuration.
+ /// The rectangle to clone.
+ /// The .
+ internal static Buffer2D CloneRegion(this Buffer2D source, Configuration configuration, Rectangle rectangle)
+ where T : unmanaged
+ {
+ Buffer2D buffer = configuration.MemoryAllocator.Allocate2D(
+ rectangle.Width,
+ rectangle.Height,
+ configuration.PreferContiguousImageBuffers);
+
+ // Optimization for when the size of the area is the same as the buffer size.
+ Buffer2DRegion sourceRegion = source.GetRegion(rectangle);
+ if (sourceRegion.IsFullBufferArea)
+ {
+ sourceRegion.Buffer.CopyTo(buffer);
+ }
+ else
+ {
+ for (int y = 0; y < rectangle.Height; y++)
+ {
+ sourceRegion.DangerousGetRowSpan(y).CopyTo(buffer.DangerousGetRowSpan(y));
+ }
+ }
+
+ return buffer;
+ }
+
///
/// TODO: Does not work with multi-buffer groups, should be specific to Resize.
- /// Copy columns of inplace,
- /// from positions starting at to positions at .
+ /// Copy columns of in-place,
+ /// from positions starting at to positions at .
///
+ /// The element type.
+ /// The .
+ /// The source column index.
+ /// The destination column index.
+ /// The number of columns to copy.
internal static unsafe void DangerousCopyColumns(
this Buffer2D buffer,
int sourceIndex,
- int destIndex,
+ int destinationIndex,
int columnCount)
where T : struct
{
DebugGuard.NotNull(buffer, nameof(buffer));
DebugGuard.MustBeGreaterThanOrEqualTo(sourceIndex, 0, nameof(sourceIndex));
- DebugGuard.MustBeGreaterThanOrEqualTo(destIndex, 0, nameof(sourceIndex));
- CheckColumnRegionsDoNotOverlap(buffer, sourceIndex, destIndex, columnCount);
+ DebugGuard.MustBeGreaterThanOrEqualTo(destinationIndex, 0, nameof(sourceIndex));
+ 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 = destIndex * elementSize;
+ int dOffset = destinationIndex * elementSize;
long count = columnCount * elementSize;
Span span = MemoryMarshal.AsBytes(buffer.DangerousGetSingleMemory().Span);
@@ -60,65 +98,45 @@ internal static unsafe void DangerousCopyColumns(
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
- {
- return new Rectangle(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)
- where T : unmanaged =>
- new Buffer2DRegion(buffer, rectangle);
-
- internal static Buffer2DRegion GetRegion(this Buffer2D buffer, int x, int y, int width, int height)
+ public static Buffer2DRegion GetRegion(this Buffer2D buffer, Rectangle rectangle)
where T : unmanaged =>
- new Buffer2DRegion(buffer, new Rectangle(x, y, width, height));
+ new(buffer, rectangle);
///
- /// Return a to the whole area of 'buffer'
+ /// Return a to the specified area of .
///
- /// The element type
- /// The
- /// The
- internal static Buffer2DRegion GetRegion(this Buffer2D buffer)
+ /// 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 Buffer2DRegion(buffer);
+ new(buffer, new Rectangle(x, y, width, height));
///
- /// Returns the size of the buffer.
+ /// Return a to the whole area of .
///
/// 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
- internal static Rectangle Bounds(this Buffer2D buffer)
- where T : struct =>
- new(0, 0, buffer.Width, buffer.Height);
+ /// The
+ public static Buffer2DRegion GetRegion(this Buffer2D buffer)
+ where T : unmanaged =>
+ new(buffer);
[Conditional("DEBUG")]
private static void CheckColumnRegionsDoNotOverlap(
diff --git a/src/ImageSharp/Memory/Buffer2DRegion{T}.cs b/src/ImageSharp/Memory/Buffer2DRegion{T}.cs
index 033b0a25a6..c78c4ce9ee 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 Buffer2DRegion(Buffer2D buffer, Rectangle rectangle)
/// 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 Buffer2DRegion(Buffer2D buffer)
///
/// 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 Buffer2DRegion(Buffer2D buffer)
/// 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 Buffer2DRegion(Buffer2D buffer)
[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);
}
@@ -107,23 +107,23 @@ public Span DangerousGetRowSpan(int y)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Buffer2DRegion GetSubRegion(int x, int y, int width, int height)
{
- var rectangle = new Rectangle(x, y, width, height);
+ Rectangle rectangle = new(x, y, width, height);
return this.GetSubRegion(rectangle);
}
///
/// 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 Buffer2DRegion GetSubRegion(Rectangle rectangle)
[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 @@ internal ref T GetReferenceToOrigin()
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 @@ internal void Clear()
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 39c6e62e15..cfd7664f01 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 @@ internal Buffer2D(MemoryGroup memoryGroup, int width, int height)
}
}
+ ///
+ /// 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 Span DangerousGetRowSpan(int y)
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 @@ internal bool DangerousTryGetPaddedRowSpan(int y, int padding, out Span padde
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 @@ internal bool DangerousTryGetPaddedRowSpan(int y, int padding, out Span padde
[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 @@ internal Memory GetSafeRowMemory(int y)
{
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 @@ internal static bool SwapOrCopyContent(Buffer2D destination, Buffer2D sour
}
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 7e9719ea75..03f26aab04 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 b4b1ffc6f4..b399d3d700 100644
--- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs
+++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs
@@ -36,8 +36,13 @@ internal static void Clear(this IMemoryGroup group)
///
/// Returns a slice that is expected to be within the bounds of a single buffer.
- /// Otherwise is thrown.
///
+ /// The type of element.
+ /// The group.
+ /// The start index of the slice.
+ /// The length of the slice.
+ /// Slice is out of bounds.
+ /// The slice.
internal static Memory GetBoundedMemorySlice(this IMemoryGroup group, long start, int length)
where T : struct
{
@@ -66,128 +71,159 @@ internal static Memory GetBoundedMemorySlice(this IMemoryGroup group, l
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));
- var cur = new MemoryGroupCursor(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);
- 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 sourceCursor = new(source);
+ int sourceSkip = sourceStride - width;
- var cur = new MemoryGroupCursor(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;
- var srcCur = new MemoryGroupCursor(source);
- var trgCur = new MemoryGroupCursor(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;
- var srcCur = new MemoryGroupCursor(source);
- var trgCur = new MemoryGroupCursor(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 950e2a019e..75e93ce7f8 100644
--- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs
+++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs
@@ -31,23 +31,23 @@ public override int Count
///
[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 9da0139e6e..c4b22f1560 100644
--- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
+++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
@@ -60,21 +60,73 @@ public override Memory this[int index]
}
}
+ 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,
int sizeOfLastBuffer,
AllocationOptions options)
{
- var result = new IMemoryOwner[pooledBuffers.Length];
+ IMemoryOwner[] result = new IMemoryOwner[pooledBuffers.Length];
for (int i = 0; i < pooledBuffers.Length - 1; i++)
{
- var currentBuffer = ObservedBuffer.Create(pooledBuffers[i], bufferLength, options);
+ ObservedBuffer currentBuffer = ObservedBuffer.Create(pooledBuffers[i], bufferLength, options);
result[i] = currentBuffer;
}
- var 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 @@ public override void Dispose()
}
}
+ this.ReleaseAllocationTracking();
this.memoryOwners = null;
this.IsValid = false;
this.groupLifetimeGuard = null;
@@ -193,7 +246,7 @@ public static ObservedBuffer Create(
int lengthInElements,
AllocationOptions options)
{
- var buffer = new ObservedBuffer(handle, lengthInElements);
+ ObservedBuffer buffer = new(handle, lengthInElements);
if (options.Has(AllocationOptions.Clean))
{
buffer.GetSpan().Clear();
diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs
index 03c29a7231..e0b9bca5e2 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 @@ private MemoryGroup(int bufferLength, long totalLength)
///
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 @@ public static MemoryGroup Allocate(
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 @@ public static MemoryGroup Allocate(
if (totalLengthInElements == 0)
{
- var buffers0 = new IMemoryOwner[1] { 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;
@@ -120,15 +141,15 @@ public static MemoryGroup Allocate(
bufferCount++;
}
- var buffers = new IMemoryOwner[bufferCount];
+ 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);
@@ -142,7 +163,7 @@ public static MemoryGroup CreateContiguous(IMemoryOwner buffer, bool clear
}
int length = buffer.Memory.Length;
- var buffers = new IMemoryOwner[1] { buffer };
+ IMemoryOwner[] buffers = [buffer];
return new Owned(buffers, length, length, true);
}
diff --git a/src/ImageSharp/Memory/InvalidMemoryOperationException.cs b/src/ImageSharp/Memory/InvalidMemoryOperationException.cs
index 81210f13db..724af35e1d 100644
--- a/src/ImageSharp/Memory/InvalidMemoryOperationException.cs
+++ b/src/ImageSharp/Memory/InvalidMemoryOperationException.cs
@@ -39,4 +39,9 @@ internal static void ThrowInvalidAlignmentException(long alignment) =>
[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 ff306e1e45..57ebcab768 100644
--- a/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs
+++ b/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs
@@ -29,6 +29,9 @@ public static Buffer2D Allocate2D(
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 @@ internal static Buffer2D Allocate2DOveraligned(
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/RowInterval.cs b/src/ImageSharp/Memory/RowInterval.cs
index 90a88d735a..f27dc74411 100644
--- a/src/ImageSharp/Memory/RowInterval.cs
+++ b/src/ImageSharp/Memory/RowInterval.cs
@@ -79,7 +79,7 @@ public override bool Equals(object? obj)
///
public override string ToString() => $"RowInterval [{this.Min}->{this.Max}]";
- internal RowInterval Slice(int start) => new RowInterval(this.Min + start, this.Max);
+ internal RowInterval Slice(int start) => new(this.Min + start, this.Max);
- internal RowInterval Slice(int start, int length) => new RowInterval(this.Min + start, this.Min + start + length);
+ internal RowInterval Slice(int start, int length) => new(this.Min + start, this.Min + start + length);
}
diff --git a/src/ImageSharp/Memory/TransformItemsDelegate{TSource, TTarget}.cs b/src/ImageSharp/Memory/TransformItemsDelegate{TSource, TTarget}.cs
deleted file mode 100644
index bc3d17f8f0..0000000000
--- 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 d1ef51fb85..0000000000
--- 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/ImageFrameMetadata.cs b/src/ImageSharp/Metadata/ImageFrameMetadata.cs
index 1c0330d5d0..554afd69aa 100644
--- a/src/ImageSharp/Metadata/ImageFrameMetadata.cs
+++ b/src/ImageSharp/Metadata/ImageFrameMetadata.cs
@@ -1,12 +1,14 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Numerics;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
+using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Metadata;
@@ -15,7 +17,7 @@ namespace SixLabors.ImageSharp.Metadata;
///
public sealed class ImageFrameMetadata : IDeepCloneable
{
- private readonly Dictionary formatMetadata = new();
+ private readonly Dictionary formatMetadata = [];
///
/// Initializes a new instance of the class.
@@ -35,9 +37,9 @@ internal ImageFrameMetadata(ImageFrameMetadata other)
{
DebugGuard.NotNull(other, nameof(other));
- foreach (KeyValuePair meta in other.formatMetadata)
+ foreach (KeyValuePair meta in other.formatMetadata)
{
- this.formatMetadata.Add(meta.Key, meta.Value.DeepClone());
+ this.formatMetadata.Add(meta.Key, (IFormatFrameMetadata)meta.Value.DeepClone());
}
this.ExifProfile = other.ExifProfile?.DeepClone();
@@ -45,6 +47,10 @@ internal ImageFrameMetadata(ImageFrameMetadata other)
this.IptcProfile = other.IptcProfile?.DeepClone();
this.XmpProfile = other.XmpProfile?.DeepClone();
this.CicpProfile = other.CicpProfile?.DeepClone();
+
+ // NOTE: This clone is actually shallow but we share the same format
+ // instances for all images in the configuration.
+ this.DecodedImageFormat = other.DecodedImageFormat;
}
///
@@ -72,12 +78,19 @@ internal ImageFrameMetadata(ImageFrameMetadata other)
///
public CicpProfile? CicpProfile { get; set; }
+ ///
+ /// Gets the original format, if any, the image was decode from.
+ ///
+ public IImageFormat? DecodedImageFormat { get; internal set; }
+
///
public ImageFrameMetadata DeepClone() => new(this);
///
- /// Gets the metadata value associated with the specified key. This method will always return a result creating
- /// a new instance and binding it to the frame metadata if none is found.
+ /// Gets the metadata value associated with the specified key.
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata
+ /// or the requested format default constructor.
+ /// This instance will be added to the metadata for future requests.
///
/// The type of format metadata.
/// The type of format frame metadata.
@@ -87,43 +100,80 @@ internal ImageFrameMetadata(ImageFrameMetadata other)
///
public TFormatFrameMetadata GetFormatMetadata(IImageFormat key)
where TFormatMetadata : class
- where TFormatFrameMetadata : class, IDeepCloneable
+ where TFormatFrameMetadata : class, IFormatFrameMetadata
{
- if (this.formatMetadata.TryGetValue(key, out IDeepCloneable? meta))
+ if (this.formatMetadata.TryGetValue(key, out IFormatFrameMetadata? meta))
{
return (TFormatFrameMetadata)meta;
}
+ // None found. Check if we have a decoded format to convert from.
+ if (this.DecodedImageFormat is not null
+ && this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatFrameMetadata? decodedMetadata))
+ {
+ TFormatFrameMetadata derivedMeta = TFormatFrameMetadata.FromFormatConnectingFrameMetadata(decodedMetadata.ToFormatConnectingFrameMetadata());
+ this.SetFormatMetadata(key, derivedMeta);
+ return derivedMeta;
+ }
+
TFormatFrameMetadata newMeta = key.CreateDefaultFormatFrameMetadata();
- this.formatMetadata[key] = newMeta;
+ this.SetFormatMetadata(key, newMeta);
return newMeta;
}
///
- /// Gets the metadata value associated with the specified key.
+ /// Sets the metadata value associated with the specified key.
///
/// The type of format metadata.
/// The type of format frame metadata.
+ /// The key of the value to set.
+ /// The value to set.
+ public void SetFormatMetadata(IImageFormat key, TFormatFrameMetadata value)
+ where TFormatMetadata : class
+ where TFormatFrameMetadata : class, IFormatFrameMetadata
+ => this.formatMetadata[key] = value;
+
+ ///
+ /// Creates a new instance the metadata value associated with the specified key.
+ /// The instance is created from a clone generated via .
+ ///
+ /// The type of metadata.
+ /// The type of format frame metadata.
/// The key of the value to get.
- ///
- /// When this method returns, contains the metadata associated with the specified key,
- /// if the key is found; otherwise, the default value for the type of the metadata parameter.
- /// This parameter is passed uninitialized.
- ///
///
- /// if the frame metadata exists for the specified key; otherwise, .
+ /// The .
///
- public bool TryGetFormatMetadata(IImageFormat key, out TFormatFrameMetadata? metadata)
+ public TFormatFrameMetadata CloneFormatMetadata(IImageFormat key)
where TFormatMetadata : class
- where TFormatFrameMetadata : class, IDeepCloneable
+ where TFormatFrameMetadata : class, IFormatFrameMetadata
+ => ((IDeepCloneable)this.GetFormatMetadata(key)).DeepClone();
+
+ ///
+ /// Synchronizes the profiles with the current metadata.
+ ///
+ internal void SynchronizeProfiles() => this.ExifProfile?.Sync(this);
+
+ ///
+ /// This method is called after a process has been applied to the image frame.
+ ///
+ /// The type of pixel format.
+ /// The source image frame.
+ /// The destination image frame.
+ /// The transformation matrix applied to the frame.
+ internal void AfterFrameApply(
+ ImageFrame source,
+ ImageFrame destination,
+ Matrix4x4 matrix)
+ where TPixel : unmanaged, IPixel
{
- if (this.formatMetadata.TryGetValue(key, out IDeepCloneable? meta))
+ // Always updated using the full frame dimensions.
+ // Individual format frame metadata will update with sub region dimensions if appropriate.
+ this.ExifProfile?.SyncDimensions(destination.Width, destination.Height);
+ this.ExifProfile?.SyncSubject(destination.Width, destination.Height, matrix);
+
+ foreach (KeyValuePair meta in this.formatMetadata)
{
- metadata = (TFormatFrameMetadata)meta;
- return true;
+ meta.Value.AfterFrameApply(source, destination, matrix);
}
-
- metadata = default;
- return false;
}
}
diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs
index 6b62be08ff..918cf162a5 100644
--- a/src/ImageSharp/Metadata/ImageMetadata.cs
+++ b/src/ImageSharp/Metadata/ImageMetadata.cs
@@ -1,12 +1,14 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Numerics;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
+using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Metadata;
@@ -33,7 +35,7 @@ public sealed class ImageMetadata : IDeepCloneable
///
public const PixelResolutionUnit DefaultPixelResolutionUnits = PixelResolutionUnit.PixelsPerInch;
- private readonly Dictionary formatMetadata = new();
+ private readonly Dictionary formatMetadata = [];
private double horizontalResolution;
private double verticalResolution;
@@ -60,9 +62,9 @@ private ImageMetadata(ImageMetadata other)
this.VerticalResolution = other.VerticalResolution;
this.ResolutionUnits = other.ResolutionUnits;
- foreach (KeyValuePair meta in other.formatMetadata)
+ foreach (KeyValuePair meta in other.formatMetadata)
{
- this.formatMetadata.Add(meta.Key, meta.Value.DeepClone());
+ this.formatMetadata.Add(meta.Key, (IFormatMetadata)meta.Value.DeepClone());
}
this.ExifProfile = other.ExifProfile?.DeepClone();
@@ -165,12 +167,15 @@ public double VerticalResolution
public CicpProfile? CicpProfile { get; set; }
///
- /// Gets the original format, if any, the image was decode from.
+ /// Gets the original format, if any, from which the image was decoded.
///
public IImageFormat? DecodedImageFormat { get; internal set; }
///
- /// Gets the metadata value associated with the specified key.
+ /// Gets the metadata value associated with the specified key.
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata
+ /// or the requested format default constructor.
+ /// This instance will be added to the metadata for future requests.
///
/// The type of metadata.
/// The key of the value to get.
@@ -178,43 +183,45 @@ public double VerticalResolution
/// The .
///
public TFormatMetadata GetFormatMetadata(IImageFormat key)
- where TFormatMetadata : class, IDeepCloneable
+ where TFormatMetadata : class, IFormatMetadata
{
- if (this.formatMetadata.TryGetValue(key, out IDeepCloneable? meta))
+ // Check for existing metadata.
+ if (this.formatMetadata.TryGetValue(key, out IFormatMetadata? meta))
{
return (TFormatMetadata)meta;
}
+ // None found. Check if we have a decoded format to convert from.
+ if (this.DecodedImageFormat is not null
+ && this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatMetadata? decodedMetadata))
+ {
+ TFormatMetadata derivedMeta = TFormatMetadata.FromFormatConnectingMetadata(decodedMetadata.ToFormatConnectingMetadata());
+ this.formatMetadata[key] = derivedMeta;
+ return derivedMeta;
+ }
+
+ // Fall back to a default instance.
TFormatMetadata newMeta = key.CreateDefaultFormatMetadata();
this.formatMetadata[key] = newMeta;
return newMeta;
}
///
- /// Gets the metadata value associated with the specified key.
+ /// Creates a new instance the metadata value associated with the specified key.
+ /// The instance is created from a clone generated via .
///
- /// The type of format metadata.
+ /// The type of metadata.
/// The key of the value to get.
- ///
- /// When this method returns, contains the metadata associated with the specified key,
- /// if the key is found; otherwise, the default value for the type of the metadata parameter.
- /// This parameter is passed uninitialized.
- ///
///
- /// if the frame metadata exists for the specified key; otherwise, .
+ /// The .
///
- public bool TryGetFormatMetadata(IImageFormat key, out TFormatMetadata? metadata)
- where TFormatMetadata : class, IDeepCloneable
- {
- if (this.formatMetadata.TryGetValue(key, out IDeepCloneable? meta))
- {
- metadata = (TFormatMetadata)meta;
- return true;
- }
+ public TFormatMetadata CloneFormatMetadata(IImageFormat key)
+ where TFormatMetadata : class, IFormatMetadata
+ => ((IDeepCloneable)this.GetFormatMetadata(key)).DeepClone();
- metadata = default;
- return false;
- }
+ internal void SetFormatMetadata(IImageFormat key, TFormatMetadata value)
+ where TFormatMetadata : class, IFormatMetadata
+ => this.formatMetadata[key] = value;
///
public ImageMetadata DeepClone() => new(this);
@@ -222,5 +229,36 @@ public bool TryGetFormatMetadata(IImageFormat
///
/// Synchronizes the profiles with the current metadata.
///
- internal void SyncProfiles() => this.ExifProfile?.Sync(this);
+ internal void SynchronizeProfiles() => this.ExifProfile?.Sync(this);
+
+ ///
+ /// This method is called after a process has been applied to the image.
+ ///
+ /// The type of pixel format.
+ /// The destination image.
+ /// The transformation matrix applied to the image.
+ internal void AfterImageApply(Image destination, Matrix4x4 matrix)
+ where TPixel : unmanaged, IPixel
+ {
+ this.ExifProfile?.SyncDimensions(destination.Width, destination.Height);
+ this.ExifProfile?.SyncSubject(destination.Width, destination.Height, matrix);
+
+ foreach (KeyValuePair meta in this.formatMetadata)
+ {
+ meta.Value.AfterImageApply(destination, matrix);
+ }
+ }
+
+ internal PixelTypeInfo GetDecodedPixelTypeInfo()
+ {
+ // None found. Check if we have a decoded format to convert from.
+ if (this.DecodedImageFormat is not null
+ && this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatMetadata? decodedMetadata))
+ {
+ return decodedMetadata.GetPixelTypeInfo();
+ }
+
+ // This should never happen.
+ return default;
+ }
}
diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs
index 725533ea5f..df6f7bdf83 100644
--- a/src/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs
+++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs
@@ -7,21 +7,21 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif;
internal static class ExifConstants
{
- public static ReadOnlySpan LittleEndianByteOrderMarker => new byte[]
- {
+ public static ReadOnlySpan LittleEndianByteOrderMarker =>
+ [
(byte)'I',
(byte)'I',
0x2A,
- 0x00,
- };
+ 0x00
+ ];
- public static ReadOnlySpan BigEndianByteOrderMarker => new byte[]
- {
+ public static ReadOnlySpan BigEndianByteOrderMarker =>
+ [
(byte)'M',
(byte)'M',
0x00,
0x2A
- };
+ ];
// UTF-8 is better than ASCII, UTF-8 encodes the ASCII codes the same way
public static Encoding DefaultEncoding => Encoding.UTF8;
diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs
index 90a5d15b74..e0b549362d 100644
--- a/src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs
+++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs
@@ -4,7 +4,7 @@
namespace SixLabors.ImageSharp.Metadata.Profiles.Exif;
///
-/// Specifies exif data types.
+/// Specifies Exif data types.
///
public enum ExifDataType
{
diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs
index e9f46731c8..d2b88cbfff 100644
--- a/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs
+++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs
@@ -16,13 +16,13 @@ internal static class ExifEncodedStringHelpers
private const ulong UnicodeCode = 0x_45_44_4F_43_49_4E_55;
private const ulong UndefinedCode = 0x_00_00_00_00_00_00_00_00;
- private static ReadOnlySpan AsciiCodeBytes => new byte[] { 0x41, 0x53, 0x43, 0x49, 0x49, 0, 0, 0 };
+ private static ReadOnlySpan AsciiCodeBytes => [0x41, 0x53, 0x43, 0x49, 0x49, 0, 0, 0];
- private static ReadOnlySpan JISCodeBytes => new byte[] { 0x4A, 0x49, 0x53, 0, 0, 0, 0, 0 };
+ private static ReadOnlySpan JISCodeBytes => [0x4A, 0x49, 0x53, 0, 0, 0, 0, 0];
- private static ReadOnlySpan UnicodeCodeBytes => new byte[] { 0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0 };
+ private static ReadOnlySpan UnicodeCodeBytes => [0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0];
- private static ReadOnlySpan UndefinedCodeBytes => new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 };
+ private static ReadOnlySpan UndefinedCodeBytes => [0, 0, 0, 0, 0, 0, 0, 0];
// 20932 EUC-JP Japanese (JIS 0208-1990 and 0212-1990)
// https://docs.microsoft.com/en-us/dotnet/api/system.text.encoding?view=net-6.0
@@ -50,22 +50,45 @@ private static Encoding JIS0208Encoding
_ => UndefinedCodeBytes
};
- public static Encoding GetEncoding(CharacterCode code) => code switch
+ public static Encoding GetEncoding(CharacterCode code, ByteOrder order) => code switch
{
CharacterCode.ASCII => Encoding.ASCII,
CharacterCode.JIS => JIS0208Encoding,
- CharacterCode.Unicode => Encoding.Unicode,
+ CharacterCode.Unicode => order is ByteOrder.BigEndian ? Encoding.BigEndianUnicode : Encoding.Unicode,
CharacterCode.Undefined => Encoding.UTF8,
_ => Encoding.UTF8
};
- public static bool TryParse(ReadOnlySpan buffer, out EncodedString encodedString)
+ public static bool TryParse(ReadOnlySpan buffer, ByteOrder order, out EncodedString encodedString)
{
if (TryDetect(buffer, out CharacterCode code))
{
- string text = GetEncoding(code).GetString(buffer[CharacterCodeBytesLength..]);
- encodedString = new EncodedString(code, text);
- return true;
+ ReadOnlySpan textBuffer = buffer[CharacterCodeBytesLength..];
+ if (code == CharacterCode.Unicode && textBuffer.Length >= 2)
+ {
+ // Check BOM
+ if (textBuffer.StartsWith((ReadOnlySpan)[0xFF, 0xFE]))
+ {
+ // Little-endian BOM
+ string text = Encoding.Unicode.GetString(textBuffer[2..]);
+ encodedString = new EncodedString(code, text);
+ return true;
+ }
+
+ if (textBuffer.StartsWith((ReadOnlySpan)[0xFE, 0xFF]))
+ {
+ // Big-endian BOM
+ string text = Encoding.BigEndianUnicode.GetString(textBuffer[2..]);
+ encodedString = new EncodedString(code, text);
+ return true;
+ }
+ }
+
+ {
+ string text = GetEncoding(code, order).GetString(textBuffer);
+ encodedString = new EncodedString(code, text);
+ return true;
+ }
}
encodedString = default;
@@ -73,14 +96,14 @@ public static bool TryParse(ReadOnlySpan buffer, out EncodedString encoded
}
public static uint GetDataLength(EncodedString encodedString) =>
- (uint)GetEncoding(encodedString.Code).GetByteCount(encodedString.Text) + CharacterCodeBytesLength;
+ (uint)GetEncoding(encodedString.Code, ByteOrder.LittleEndian).GetByteCount(encodedString.Text) + CharacterCodeBytesLength;
public static int Write(EncodedString encodedString, Span destination)
{
GetCodeBytes(encodedString.Code).CopyTo(destination);
string text = encodedString.Text;
- int count = Write(GetEncoding(encodedString.Code), text, destination[CharacterCodeBytesLength..]);
+ int count = Write(GetEncoding(encodedString.Code, ByteOrder.LittleEndian), text, destination[CharacterCodeBytesLength..]);
return CharacterCodeBytesLength + count;
}
@@ -92,8 +115,7 @@ private static bool TryDetect(ReadOnlySpan buffer, out CharacterCode code)
{
if (buffer.Length >= CharacterCodeBytesLength)
{
- ulong test = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
- switch (test)
+ switch (BinaryPrimitives.ReadUInt64LittleEndian(buffer))
{
case AsciiCode:
code = CharacterCode.ASCII;
@@ -108,7 +130,8 @@ private static bool TryDetect(ReadOnlySpan buffer, out CharacterCode code)
code = CharacterCode.Undefined;
return true;
default:
- break;
+ code = default;
+ return false;
}
}
diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
index dd5792ae79..6cd16aa9c2 100644
--- a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
+++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
@@ -2,7 +2,9 @@
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing.Processors.Transforms;
namespace SixLabors.ImageSharp.Metadata.Profiles.Exif;
@@ -47,7 +49,7 @@ public ExifProfile(byte[]? data)
{
this.Parts = ExifParts.All;
this.data = data;
- this.InvalidTags = Array.Empty();
+ this.InvalidTags = [];
}
///
@@ -160,17 +162,15 @@ public bool TryCreateThumbnail([NotNullWhen(true)] out Image? im
return false;
}
- using (MemoryStream memStream = new(this.data, this.thumbnailOffset, this.thumbnailLength))
- {
- image = Image.Load(memStream);
- return true;
- }
+ using MemoryStream memStream = new(this.data, this.thumbnailOffset, this.thumbnailLength);
+ image = Image.Load(memStream);
+ return true;
}
///
/// Returns the value with the specified tag.
///
- /// The tag of the exif value.
+ /// The tag of the Exif value.
/// The value with the specified tag.
/// True when found, otherwise false
/// The data type of the tag.
@@ -214,26 +214,33 @@ public bool RemoveValue(ExifTag tag)
///
/// Sets the value of the specified tag.
///
- /// The tag of the exif value.
+ /// The tag of the Exif value.
/// The value.
/// The data type of the tag.
public void SetValue(ExifTag tag, TValueType value)
=> this.SetValueInternal(tag, value);
///
- /// Converts this instance to a byte array.
+ /// Converts the sections selected by to a byte array.
///
/// The
public byte[]? ToByteArray()
{
if (this.values is null)
{
- return this.data;
+ // The original bytes include every section. They can only be reused when no filtering
+ // is requested; otherwise lazy profiles must go through the same writer as initialized ones.
+ if (this.Parts == ExifParts.All)
+ {
+ return this.data;
+ }
+
+ this.InitializeValues();
}
if (this.values.Count == 0)
{
- return Array.Empty();
+ return [];
}
ExifWriter writer = new(this.values, this.Parts);
@@ -246,7 +253,7 @@ public void SetValue(ExifTag tag, TValueType value)
///
/// Returns the value with the specified tag.
///
- /// The tag of the exif value.
+ /// The tag of the Exif value.
/// The value with the specified tag.
internal IExifValue? GetValueInternal(ExifTag tag)
{
@@ -264,9 +271,9 @@ public void SetValue(ExifTag tag, TValueType value)
///
/// Sets the value of the specified tag.
///
- /// The tag of the exif value.
+ /// The tag of the Exif value.
/// The value.
- /// Newly created value is null.
+ /// The newly created value is null.
internal void SetValueInternal(ExifTag tag, object? value)
{
foreach (IExifValue exifValue in this.Values)
@@ -278,11 +285,7 @@ internal void SetValueInternal(ExifTag tag, object? value)
}
}
- ExifValue? newExifValue = ExifValues.Create(tag);
- if (newExifValue is null)
- {
- throw new NotSupportedException($"Newly created value for tag {tag} is null.");
- }
+ ExifValue? newExifValue = ExifValues.Create(tag) ?? throw new NotSupportedException($"Newly created value for tag {tag} is null.");
newExifValue.TrySetValue(value);
this.values.Add(newExifValue);
@@ -298,6 +301,84 @@ internal void Sync(ImageMetadata metadata)
this.SyncResolution(ExifTag.YResolution, metadata.VerticalResolution);
}
+ internal void SyncDimensions(int width, int height)
+ {
+ if (this.TryGetValue(ExifTag.PixelXDimension, out _))
+ {
+ this.SetValue(ExifTag.PixelXDimension, width);
+ }
+
+ if (this.TryGetValue(ExifTag.PixelYDimension, out _))
+ {
+ this.SetValue(ExifTag.PixelYDimension, height);
+ }
+ }
+
+ internal void SyncSubject(int width, int height, Matrix4x4 matrix)
+ {
+ if (matrix.IsIdentity)
+ {
+ return;
+ }
+
+ if (this.TryGetValue(ExifTag.SubjectLocation, out IExifValue? location))
+ {
+ if (location.Value?.Length == 2)
+ {
+ 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));
+
+ // Floor the point to the nearest pixel.
+ location.Value[0] = (ushort)Math.Floor(point.X);
+ location.Value[1] = (ushort)Math.Floor(point.Y);
+
+ this.SetValue(ExifTag.SubjectLocation, location.Value);
+ }
+ else
+ {
+ this.RemoveValue(ExifTag.SubjectLocation);
+ }
+ }
+
+ if (this.TryGetValue(ExifTag.SubjectArea, out IExifValue? area))
+ {
+ if (area.Value?.Length == 4)
+ {
+ RectangleF rectangle = new(area.Value[0], area.Value[1], area.Value[2], area.Value[3]);
+ if (!TransformUtilities.TryGetTransformedRectangle(rectangle, matrix, out RectangleF bounds))
+ {
+ return;
+ }
+
+ // Ensure the bounds are within the image dimensions.
+ bounds = RectangleF.Intersect(bounds, new Rectangle(0, 0, width, 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
+ {
+ this.RemoveValue(ExifTag.SubjectArea);
+ }
+ }
+ }
+
+ ///
+ /// Synchronizes the profiles with the specified metadata.
+ ///
+ /// The metadata.
+#pragma warning disable CA1822, RCS1163, IDE0060
+ internal void Sync(ImageFrameMetadata metadata)
+#pragma warning restore IDE0060, RCS1163, CA1822
+ {
+ // Nothing to do ....YET.
+ }
+
private void SyncResolution(ExifTag tag, double resolution)
{
if (!this.TryGetValue(tag, out IExifValue? value))
@@ -324,7 +405,7 @@ private void InitializeValues()
if (this.data is null)
{
- this.values = new List();
+ this.values = [];
return;
}
diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs
index 953ef74afb..4e2315cd88 100644
--- a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs
+++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs
@@ -7,6 +7,7 @@
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using System.Text;
using SixLabors.ImageSharp.Memory;
@@ -33,7 +34,7 @@ public ExifReader(byte[] exifData, MemoryAllocator? allocator)
///
public List ReadValues()
{
- List values = new();
+ List values = [];
// II == 0x4949
this.IsBigEndian = this.ReadUInt16() != 0x4949;
@@ -63,7 +64,7 @@ private void GetThumbnail(uint offset)
return;
}
- List values = new();
+ List values = [];
this.ReadValues(values, offset);
for (int i = 0; i < values.Count; i++)
@@ -89,8 +90,8 @@ internal abstract class BaseExifReader
private readonly MemoryAllocator? allocator;
private readonly Stream data;
private List? invalidTags;
-
private List? subIfds;
+ private bool isBigEndian;
protected BaseExifReader(Stream stream, MemoryAllocator? allocator)
{
@@ -103,7 +104,7 @@ protected BaseExifReader(Stream stream, MemoryAllocator? allocator)
///
/// Gets the invalid tags.
///
- public IReadOnlyList InvalidTags => this.invalidTags ?? (IReadOnlyList)Array.Empty();
+ public IReadOnlyList InvalidTags => this.invalidTags ?? (IReadOnlyList)[];
///
/// Gets or sets the thumbnail length in the byte stream.
@@ -115,9 +116,19 @@ protected BaseExifReader(Stream stream, MemoryAllocator? allocator)
///
public uint ThumbnailOffset { get; protected set; }
- public bool IsBigEndian { get; protected set; }
+ public bool IsBigEndian
+ {
+ get => this.isBigEndian;
+ protected set
+ {
+ this.isBigEndian = value;
+ this.ByteOrder = value ? ByteOrder.BigEndian : ByteOrder.LittleEndian;
+ }
+ }
- public List<(ulong Offset, ExifDataType DataType, ulong NumberOfComponents, ExifValue Exif)> BigValues { get; } = new();
+ protected ByteOrder ByteOrder { get; private set; }
+
+ public List<(ulong Offset, ExifDataType DataType, ulong NumberOfComponents, ExifValue Exif)> BigValues { get; } = [];
protected void ReadBigValues(List values)
{
@@ -187,11 +198,21 @@ protected void ReadValues(List values, uint offset)
protected void ReadSubIfd(List values)
{
- if (this.subIfds is not null)
+ if (this.subIfds != null)
{
- foreach (ulong subIfdOffset in this.subIfds)
+ const int maxSubIfds = 8;
+ const int maxNestingLevel = 8;
+ Span buf = stackalloc ulong[maxSubIfds];
+ for (int i = 0; i < maxNestingLevel && this.subIfds.Count > 0; i++)
{
- this.ReadValues(values, (uint)subIfdOffset);
+ int sz = Math.Min(this.subIfds.Count, maxSubIfds);
+ CollectionsMarshal.AsSpan(this.subIfds)[..sz].CopyTo(buf);
+
+ this.subIfds.Clear();
+ foreach (ulong subIfdOffset in buf[..sz])
+ {
+ this.ReadValues(values, (uint)subIfdOffset);
+ }
}
}
}
@@ -203,6 +224,13 @@ protected void ReadValues64(List values, ulong offset)
this.Seek(offset);
ulong count = this.ReadUInt64();
+ // Each entry occupies 20 bytes and the directory ends with an 8-byte next-IFD offset.
+ long remainingDirectoryBytes = this.data.Length - this.data.Position;
+ if (remainingDirectoryBytes < 8 || count > (ulong)((remainingDirectoryBytes - 8) / 20))
+ {
+ throw new InvalidImageContentException("The BigTIFF directory entry count exceeds the available data.");
+ }
+
Span offsetBuffer = stackalloc byte[8];
for (ulong i = 0; i < count; i++)
{
@@ -447,6 +475,7 @@ private void ReadValue64(List values, Span offsetBuffer)
ExifTagValue.TileByteCounts => new ExifLong8Array(ExifTagValue.TileByteCounts),
_ => ExifValues.Create(tag) ?? ExifValues.Create(tag, dataType, numberOfComponents),
};
+
if (exifValue is null)
{
this.AddInvalidTag(new UnkownExifTag(tag));
@@ -472,17 +501,25 @@ private void ReadValue64(List values, Span offsetBuffer)
}
}
- private void Add(IList values, IExifValue exif, object? value)
+ private void Add(IList values, ExifValue exif, object? value)
{
- if (!exif.TrySetValue(value))
+ if (exif is ExifEncodedString encodedString)
+ {
+ if (!encodedString.TrySetValue(value, this.ByteOrder))
+ {
+ return;
+ }
+ }
+ else if (!exif.TrySetValue(value))
{
return;
}
foreach (IExifValue val in values)
{
- // Sometimes duplicates appear, can compare val.Tag == exif.Tag
- if (val == exif)
+ // To skip duplicates must be used Equals method,
+ // == operator not defined for ExifValue and IExifValue
+ if (exif.Equals(val))
{
Debug.WriteLine($"Duplicate Exif tag: tag={exif.Tag}, dataType={exif.DataType}");
return;
@@ -504,10 +541,10 @@ private void Add(IList values, IExifValue exif, object? value)
}
private void AddInvalidTag(ExifTag tag)
- => (this.invalidTags ??= new List()).Add(tag);
+ => (this.invalidTags ??= []).Add(tag);
private void AddSubIfd(object? val)
- => (this.subIfds ??= new List()).Add(Convert.ToUInt64(val, CultureInfo.InvariantCulture));
+ => (this.subIfds ??= []).Add(Convert.ToUInt64(val, CultureInfo.InvariantCulture));
private void Seek(ulong pos)
=> this.data.Seek((long)pos, SeekOrigin.Begin);
diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs
index 1d2dca8700..659df01a65 100644
--- a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs
+++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs
@@ -55,7 +55,7 @@ public byte[] GetData()
if (length == 0)
{
- return Array.Empty();
+ return [];
}
// two bytes for the byte Order marker 'II' or 'MM', followed by the number 42 (0x2A) and a 0, making 4 bytes total
@@ -197,7 +197,7 @@ private static int WriteInt32(int value, Span destination, int offset)
private List GetPartValues(ExifParts part)
{
- List result = new();
+ List result = [];
if (!EnumUtils.HasFlag(this.allowedParts, part))
{
@@ -241,7 +241,7 @@ private static bool HasValue(IExifValue exifValue)
return true;
}
- private static uint GetLength(IList values)
+ private static uint GetLength(List values)
{
if (values.Count == 0)
{
@@ -332,7 +332,7 @@ private int WriteData(uint startIndex, List values, Span desti
private int WriteHeaders(List values, Span destination, int offset)
{
- this.dataOffsets = new List();
+ this.dataOffsets = [];
int newOffset = WriteUInt16((ushort)values.Count, destination, offset);
@@ -403,6 +403,13 @@ private static int WriteValue(ExifDataType dataType, object value, Span de
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/Exif/Tags/ExifTag.Byte.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Byte.cs
index 9ee2cf2f45..ff74ecd19d 100644
--- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Byte.cs
+++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Byte.cs
@@ -9,15 +9,15 @@ public abstract partial class ExifTag
///
/// Gets the FaxProfile exif tag.
///
- public static ExifTag FaxProfile { get; } = new ExifTag(ExifTagValue.FaxProfile);
+ public static ExifTag FaxProfile { get; } = new(ExifTagValue.FaxProfile);
///
/// Gets the ModeNumber exif tag.
///
- public static ExifTag ModeNumber { get; } = new ExifTag(ExifTagValue.ModeNumber);
+ public static ExifTag ModeNumber { get; } = new(ExifTagValue.ModeNumber);
///
/// Gets the GPSAltitudeRef exif tag.
///
- public static ExifTag GPSAltitudeRef { get; } = new ExifTag(ExifTagValue.GPSAltitudeRef);
+ public static ExifTag GPSAltitudeRef { get; } = new(ExifTagValue.GPSAltitudeRef);
}
diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ByteArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ByteArray.cs
index 00a9056d34..64d8e14371 100644
--- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ByteArray.cs
+++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ByteArray.cs
@@ -9,40 +9,40 @@ public abstract partial class ExifTag
///
/// Gets the ClipPath exif tag.
///
- public static ExifTag ClipPath => new ExifTag(ExifTagValue.ClipPath);
+ public static ExifTag ClipPath => new(ExifTagValue.ClipPath);
///
/// Gets the VersionYear exif tag.
///
- public static ExifTag VersionYear => new ExifTag(ExifTagValue.VersionYear);
+ public static ExifTag VersionYear => new(ExifTagValue.VersionYear);
///
/// Gets the XMP exif tag.
///
- public static ExifTag XMP => new ExifTag(ExifTagValue.XMP);
+ public static ExifTag XMP => new(ExifTagValue.XMP);
///
/// Gets the IPTC exif tag.
///
- public static ExifTag IPTC => new ExifTag(ExifTagValue.IPTC);
+ public static ExifTag IPTC => new(ExifTagValue.IPTC);
///
/// Gets the IccProfile exif tag.
///
- public static ExifTag IccProfile => new ExifTag(ExifTagValue.IccProfile);
+ public static ExifTag IccProfile => new(ExifTagValue.IccProfile);
///
/// Gets the CFAPattern2 exif tag.
///
- public static ExifTag CFAPattern2 => new ExifTag(ExifTagValue.CFAPattern2);
+ public static ExifTag CFAPattern2 => new(ExifTagValue.CFAPattern2);
///
/// Gets the TIFFEPStandardID exif tag.
///
- public static ExifTag TIFFEPStandardID => new ExifTag(ExifTagValue.TIFFEPStandardID);
+ public static ExifTag TIFFEPStandardID => new(ExifTagValue.TIFFEPStandardID);
///
/// Gets the GPSVersionID exif tag.
///
- public static ExifTag GPSVersionID => new ExifTag(ExifTagValue.GPSVersionID);
+ public static ExifTag GPSVersionID => new(ExifTagValue.GPSVersionID);
}
diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.DoubleArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.DoubleArray.cs
index 91d0c97b38..fded122613 100644
--- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.DoubleArray.cs
+++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.DoubleArray.cs
@@ -9,20 +9,20 @@ public abstract partial class ExifTag
///
/// Gets the PixelScale exif tag.
///
- public static ExifTag PixelScale { get; } = new ExifTag(ExifTagValue.PixelScale);
+ public static ExifTag PixelScale { get; } = new(ExifTagValue.PixelScale);
///
/// Gets the IntergraphMatrix exif tag.
///
- public static ExifTag IntergraphMatrix { get; } = new ExifTag(ExifTagValue.IntergraphMatrix);
+ public static ExifTag IntergraphMatrix { get; } = new(ExifTagValue.IntergraphMatrix);
///
/// Gets the ModelTiePoint exif tag.
///
- public static ExifTag ModelTiePoint { get; } = new ExifTag(ExifTagValue.ModelTiePoint);
+ public static ExifTag ModelTiePoint { get; } = new(ExifTagValue.ModelTiePoint);
///
/// Gets the ModelTransform exif tag.
///
- public static ExifTag ModelTransform { get; } = new ExifTag(ExifTagValue.ModelTransform);
+ public static ExifTag ModelTransform { get; } = new(ExifTagValue.ModelTransform);
}
diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.EncodedString.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.EncodedString.cs
index 4b53ba6360..4cac0d0cab 100644
--- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.EncodedString.cs
+++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.EncodedString.cs
@@ -9,15 +9,15 @@ public abstract partial class ExifTag
///
/// Gets the UserComment exif tag.
///
- public static ExifTag UserComment { get; } = new ExifTag(ExifTagValue.UserComment);
+ public static ExifTag UserComment { get; } = new(ExifTagValue.UserComment);
///
/// Gets the GPSProcessingMethod exif tag.
///
- public static ExifTag GPSProcessingMethod { get; } = new ExifTag(ExifTagValue.GPSProcessingMethod);
+ public static ExifTag GPSProcessingMethod { get; } = new(ExifTagValue.GPSProcessingMethod);
///
/// Gets the GPSAreaInformation exif tag.
///
- public static ExifTag GPSAreaInformation { get; } = new ExifTag(ExifTagValue.GPSAreaInformation);
+ public static ExifTag GPSAreaInformation { get; } = new(ExifTagValue.GPSAreaInformation);
}
diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Long.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Long.cs
index f6c7c8ea73..5f9dfb4cde 100644
--- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Long.cs
+++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Long.cs
@@ -9,105 +9,105 @@ public abstract partial class ExifTag
///
/// Gets the SubfileType exif tag.
///
- public static ExifTag SubfileType { get; } = new ExifTag(ExifTagValue.SubfileType);
+ public static ExifTag SubfileType { get; } = new(ExifTagValue.SubfileType);
///
/// Gets the SubIFDOffset exif tag.
///
- public static ExifTag SubIFDOffset { get; } = new ExifTag(ExifTagValue.SubIFDOffset);
+ public static ExifTag SubIFDOffset { get; } = new(ExifTagValue.SubIFDOffset);
///
/// Gets the GPSIFDOffset exif tag.
///
- public static ExifTag GPSIFDOffset { get; } = new ExifTag(ExifTagValue.GPSIFDOffset);
+ public static ExifTag GPSIFDOffset { get; } = new(ExifTagValue.GPSIFDOffset);
///
/// Gets the T4Options exif tag.
///
- public static ExifTag T4Options { get; } = new ExifTag(ExifTagValue.T4Options);
+ public static ExifTag T4Options { get; } = new(ExifTagValue.T4Options);
///
/// Gets the T6Options exif tag.
///
- public static ExifTag T6Options { get; } = new ExifTag(ExifTagValue.T6Options);
+ public static ExifTag T6Options { get; } = new(ExifTagValue.T6Options);
///
/// Gets the XClipPathUnits exif tag.
///
- public static ExifTag XClipPathUnits { get; } = new ExifTag(ExifTagValue.XClipPathUnits);
+ public static ExifTag XClipPathUnits { get; } = new(ExifTagValue.XClipPathUnits);
///
/// Gets the YClipPathUnits exif tag.
///
- public static ExifTag YClipPathUnits { get; } = new ExifTag(ExifTagValue.YClipPathUnits);
+ public static ExifTag YClipPathUnits { get; } = new(ExifTagValue.YClipPathUnits);
///
/// Gets the ProfileType exif tag.
///
- public static ExifTag ProfileType { get; } = new ExifTag(ExifTagValue.ProfileType);
+ public static ExifTag ProfileType { get; } = new(ExifTagValue.ProfileType);
///
/// Gets the CodingMethods exif tag.
///
- public static ExifTag CodingMethods { get; } = new ExifTag(ExifTagValue.CodingMethods);
+ public static ExifTag CodingMethods { get; } = new(ExifTagValue.CodingMethods);
///
/// Gets the T82ptions exif tag.
///
- public static ExifTag T82ptions { get; } = new ExifTag(ExifTagValue.T82ptions);
+ public static ExifTag T82ptions { get; } = new(ExifTagValue.T82ptions);
///
/// Gets the JPEGInterchangeFormat exif tag.
///
- public static ExifTag JPEGInterchangeFormat { get; } = new ExifTag(ExifTagValue.JPEGInterchangeFormat);
+ public static ExifTag JPEGInterchangeFormat { get; } = new(ExifTagValue.JPEGInterchangeFormat);
///
/// Gets the JPEGInterchangeFormatLength exif tag.
///
- public static ExifTag JPEGInterchangeFormatLength { get; } = new ExifTag(ExifTagValue.JPEGInterchangeFormatLength);
+ public static ExifTag JPEGInterchangeFormatLength { get; } = new(ExifTagValue.JPEGInterchangeFormatLength);
///
/// Gets the MDFileTag exif tag.
///
- public static ExifTag MDFileTag { get; } = new ExifTag(ExifTagValue.MDFileTag);
+ public static ExifTag MDFileTag { get; } = new(ExifTagValue.MDFileTag);
///
/// Gets the StandardOutputSensitivity exif tag.
///
- public static ExifTag StandardOutputSensitivity { get; } = new ExifTag(ExifTagValue.StandardOutputSensitivity);
+ public static ExifTag StandardOutputSensitivity { get; } = new(ExifTagValue.StandardOutputSensitivity);
///
/// Gets the RecommendedExposureIndex exif tag.
///
- public static ExifTag RecommendedExposureIndex { get; } = new ExifTag(ExifTagValue.RecommendedExposureIndex);
+ public static ExifTag RecommendedExposureIndex { get; } = new(ExifTagValue.RecommendedExposureIndex);
///
/// Gets the ISOSpeed exif tag.
///
- public static ExifTag ISOSpeed { get; } = new ExifTag(ExifTagValue.ISOSpeed);
+ public static ExifTag ISOSpeed { get; } = new(ExifTagValue.ISOSpeed);
///
/// Gets the ISOSpeedLatitudeyyy exif tag.
///
- public static ExifTag ISOSpeedLatitudeyyy { get; } = new ExifTag(ExifTagValue.ISOSpeedLatitudeyyy);
+ public static ExifTag ISOSpeedLatitudeyyy { get; } = new(ExifTagValue.ISOSpeedLatitudeyyy);
///
/// Gets the ISOSpeedLatitudezzz exif tag.
///
- public static ExifTag ISOSpeedLatitudezzz { get; } = new ExifTag(ExifTagValue.ISOSpeedLatitudezzz);
+ public static ExifTag ISOSpeedLatitudezzz { get; } = new(ExifTagValue.ISOSpeedLatitudezzz);
///
/// Gets the FaxRecvParams exif tag.
///
- public static ExifTag FaxRecvParams { get; } = new ExifTag(ExifTagValue.FaxRecvParams);
+ public static ExifTag FaxRecvParams { get; } = new(ExifTagValue.FaxRecvParams);
///
/// Gets the FaxRecvTime exif tag.
///
- public static ExifTag FaxRecvTime { get; } = new ExifTag(ExifTagValue.FaxRecvTime);
+ public static ExifTag FaxRecvTime { get; } = new(ExifTagValue.FaxRecvTime);
///
/// Gets the ImageNumber exif tag.
///
- public static ExifTag ImageNumber { get; } = new ExifTag(ExifTagValue.ImageNumber);
+ public static ExifTag ImageNumber { get; } = new(ExifTagValue.ImageNumber);
}
diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs
index 4767ca852e..e6821651ac 100644
--- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs
+++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs
@@ -9,55 +9,55 @@ public abstract partial class ExifTag
///
/// Gets the FreeOffsets exif tag.
///
- public static ExifTag FreeOffsets { get; } = new ExifTag(ExifTagValue.FreeOffsets);
+ public static ExifTag FreeOffsets { get; } = new(ExifTagValue.FreeOffsets);
///
/// Gets the FreeByteCounts exif tag.
///
- public static ExifTag FreeByteCounts { get; } = new ExifTag(ExifTagValue.FreeByteCounts);
+ public static ExifTag FreeByteCounts { get; } = new(ExifTagValue.FreeByteCounts);
///