Browse Source

Merge branch 'main' into js/resize-map-optimizations

pull/2793/head
James Jackson-South 3 months ago
parent
commit
a412805f7d
  1. 6
      .editorconfig
  2. 111
      .github/workflows/build-and-test.yml
  3. 28
      .github/workflows/code-coverage.yml
  4. 6
      Directory.Build.props
  5. 2
      shared-infrastructure
  6. 4
      src/ImageSharp/Advanced/ParallelRowIterator.Wrappers.cs
  7. 28
      src/ImageSharp/Advanced/ParallelRowIterator.cs
  8. 8
      src/ImageSharp/Color/Color.WebSafePalette.cs
  9. 8
      src/ImageSharp/Color/Color.WernerPalette.cs
  10. 394
      src/ImageSharp/Color/Color.cs
  11. 40
      src/ImageSharp/Color/ColorHexFormat.cs
  12. 2
      src/ImageSharp/ColorProfiles/CieLab.cs
  13. 2
      src/ImageSharp/ColorProfiles/ColorProfileConverter.cs
  14. 32
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs
  15. 32
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs
  16. 32
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs
  17. 32
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs
  18. 32
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs
  19. 32
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs
  20. 49
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
  21. 192
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs
  22. 32
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs
  23. 32
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs
  24. 32
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs
  25. 2
      src/ImageSharp/ColorProfiles/Icc/Calculators/ColorTrcCalculator.cs
  26. 8
      src/ImageSharp/ColorProfiles/KnownIlluminants.cs
  27. 184
      src/ImageSharp/ColorProfiles/Rgb.cs
  28. 2
      src/ImageSharp/ColorProfiles/VonKriesChromaticAdaptation.cs
  29. 2
      src/ImageSharp/ColorProfiles/Y.cs
  30. 8
      src/ImageSharp/Common/Helpers/HexConverter.cs
  31. 12
      src/ImageSharp/Common/Helpers/Numerics.cs
  32. 2
      src/ImageSharp/Common/Helpers/TolerantMath.cs
  33. 2
      src/ImageSharp/Common/InlineArray.cs
  34. 2
      src/ImageSharp/Common/InlineArray.tt
  35. 6
      src/ImageSharp/Compression/Zlib/Adler32.cs
  36. 10
      src/ImageSharp/Compression/Zlib/DeflaterConstants.cs
  37. 36
      src/ImageSharp/Compression/Zlib/DeflaterHuffman.cs
  38. 2
      src/ImageSharp/Configuration.cs
  39. 8
      src/ImageSharp/Formats/Bmp/BmpConstants.cs
  40. 2
      src/ImageSharp/Formats/Bmp/BmpDecoder.cs
  41. 6
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  42. 6
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  43. 2
      src/ImageSharp/Formats/Bmp/BmpFormat.cs
  44. 3
      src/ImageSharp/Formats/Bmp/BmpMetadata.cs
  45. 5
      src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
  46. 3
      src/ImageSharp/Formats/Cur/CurMetadata.cs
  47. 10
      src/ImageSharp/Formats/DecoderOptions.cs
  48. 22
      src/ImageSharp/Formats/Gif/GifConstants.cs
  49. 84
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  50. 88
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  51. 5
      src/ImageSharp/Formats/Gif/GifFrameMetadata.cs
  52. 3
      src/ImageSharp/Formats/Gif/GifMetadata.cs
  53. 4
      src/ImageSharp/Formats/Gif/LzwEncoder.cs
  54. 2
      src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs
  55. 4
      src/ImageSharp/Formats/IFormatFrameMetadata.cs
  56. 4
      src/ImageSharp/Formats/IFormatMetadata.cs
  57. 5
      src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
  58. 3
      src/ImageSharp/Formats/Ico/IcoMetadata.cs
  59. 12
      src/ImageSharp/Formats/Icon/IconDecoderCore.cs
  60. 6
      src/ImageSharp/Formats/Icon/IconEncoderCore.cs
  61. 16
      src/ImageSharp/Formats/ImageDecoder.cs
  62. 93
      src/ImageSharp/Formats/ImageDecoderCore.cs
  63. 2
      src/ImageSharp/Formats/ImageFormatManager.cs
  64. 2
      src/ImageSharp/Formats/Jpeg/Components/Block8x8.cs
  65. 8
      src/ImageSharp/Formats/Jpeg/Components/Block8x8F.ScaledCopy.cs
  66. 4
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKScalar.cs
  67. 2
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YCbCrScalar.cs
  68. 2
      src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.YccKScalar.cs
  69. 54
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs
  70. 2
      src/ImageSharp/Formats/Jpeg/Components/Encoder/ComponentProcessor.cs
  71. 2
      src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs
  72. 40
      src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanSpec.cs
  73. 4
      src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs
  74. 8
      src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.cs
  75. 16
      src/ImageSharp/Formats/Jpeg/Components/Quantization.cs
  76. 6
      src/ImageSharp/Formats/Jpeg/Components/SizeExtensions.cs
  77. 12
      src/ImageSharp/Formats/Jpeg/Components/ZigZag.cs
  78. 4
      src/ImageSharp/Formats/Jpeg/JpegConstants.cs
  79. 2
      src/ImageSharp/Formats/Jpeg/JpegDecoder.cs
  80. 23
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  81. 88
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.FrameConfig.cs
  82. 2
      src/ImageSharp/Formats/Jpeg/JpegFormat.cs
  83. 3
      src/ImageSharp/Formats/Jpeg/JpegMetadata.cs
  84. 4
      src/ImageSharp/Formats/Pbm/PbmConstants.cs
  85. 2
      src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs
  86. 3
      src/ImageSharp/Formats/Pbm/PbmMetadata.cs
  87. 8
      src/ImageSharp/Formats/Png/Adam7.cs
  88. 68
      src/ImageSharp/Formats/Png/PngConstants.cs
  89. 2
      src/ImageSharp/Formats/Png/PngDecoder.cs
  90. 233
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  91. 2
      src/ImageSharp/Formats/Png/PngDecoderOptions.cs
  92. 161
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  93. 2
      src/ImageSharp/Formats/Png/PngFormat.cs
  94. 9
      src/ImageSharp/Formats/Png/PngFrameMetadata.cs
  95. 26
      src/ImageSharp/Formats/Png/PngMetadata.cs
  96. 12
      src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
  97. 4
      src/ImageSharp/Formats/Qoi/QoiConstants.cs
  98. 2
      src/ImageSharp/Formats/Qoi/QoiFormat.cs
  99. 3
      src/ImageSharp/Formats/Qoi/QoiMetadata.cs
  100. 4
      src/ImageSharp/Formats/Tga/TgaConstants.cs

6
.editorconfig

@ -161,6 +161,9 @@ csharp_style_deconstructed_variable_declaration = true:warning
csharp_style_prefer_index_operator = true:warning csharp_style_prefer_index_operator = true:warning
csharp_style_prefer_range_operator = true:warning csharp_style_prefer_range_operator = true:warning
csharp_style_implicit_object_creation_when_type_is_apparent = true:error 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 # "Null" checking preferences
csharp_style_throw_expression = true:warning csharp_style_throw_expression = true:warning
csharp_style_conditional_delegate_call = true:warning csharp_style_conditional_delegate_call = true:warning
@ -174,6 +177,9 @@ csharp_using_directive_placement = outside_namespace:warning
csharp_prefer_static_local_function = true:warning csharp_prefer_static_local_function = true:warning
# Primary constructor preferences # Primary constructor preferences
csharp_style_prefer_primary_constructors = false:none 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 # Unnecessary Code Rules

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

@ -11,35 +11,79 @@ on:
branches: branches:
- main - main
- release/* - release/*
types: [ labeled, opened, synchronize, reopened ] types: [ opened, synchronize, reopened ]
jobs: 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@v4
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@v4
with:
path: .git/lfs
key: ${{ steps.expose-key.outputs.lfs_key }}
- name: Git Pull LFS
shell: bash
run: git lfs pull
Build: Build:
needs: WarmLFS
strategy: strategy:
matrix: matrix:
isARM:
- ${{ contains(github.event.pull_request.labels.*.name, 'arch:arm32') || contains(github.event.pull_request.labels.*.name, 'arch:arm64') }}
options: options:
- os: ubuntu-latest - os: ubuntu-latest
framework: net9.0 framework: net10.0
sdk: 9.0.x sdk: 10.0.x
sdk-preview: true sdk-preview: true
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable - os: macos-26
framework: net9.0 framework: net10.0
sdk: 9.0.x sdk: 10.0.x
sdk-preview: true sdk-preview: true
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: windows-latest - os: windows-latest
framework: net9.0 framework: net10.0
sdk: 9.0.x sdk: 10.0.x
sdk-preview: true sdk-preview: true
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: buildjet-4vcpu-ubuntu-2204-arm - os: ubuntu-22.04-arm
framework: net9.0 framework: net10.0
sdk: 9.0.x sdk: 10.0.x
sdk-preview: true sdk-preview: true
runtime: -x64 runtime: -x64
codecov: false codecov: false
@ -49,7 +93,7 @@ jobs:
sdk: 8.0.x sdk: 8.0.x
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable - os: macos-26
framework: net8.0 framework: net8.0
sdk: 8.0.x sdk: 8.0.x
runtime: -x64 runtime: -x64
@ -59,24 +103,32 @@ jobs:
sdk: 8.0.x sdk: 8.0.x
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: buildjet-4vcpu-ubuntu-2204-arm - os: ubuntu-22.04-arm
framework: net8.0 framework: net8.0
sdk: 8.0.x sdk: 8.0.x
runtime: -x64 runtime: -x64
codecov: false codecov: false
exclude:
- isARM: false
options:
os: buildjet-4vcpu-ubuntu-2204-arm
runs-on: ${{matrix.options.os}} runs-on: ${{ matrix.options.os }}
steps: steps:
- name: Install libgdi+, which is required for tests running on ubuntu - name: Install libgdi+, which is required for tests running on ubuntu
if: ${{ contains(matrix.options.os, 'ubuntu') }} if: ${{ contains(matrix.options.os, 'ubuntu') }}
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev 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 - name: Git Config
shell: bash shell: bash
@ -90,18 +142,15 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: recursive submodules: recursive
# See https://github.com/actions/checkout/issues/165#issuecomment-657673315 # Use the warmed key from WarmLFS. Do not recompute or recreate .lfs-assets-id here.
- name: Git Create LFS FileList
run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id
- name: Git Setup LFS Cache - name: Git Setup LFS Cache
uses: actions/cache@v4 uses: actions/cache@v4
id: lfs-cache
with: with:
path: .git/lfs path: .git/lfs
key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1 key: ${{ needs.WarmLFS.outputs.lfs_key }}
- name: Git Pull LFS - name: Git Pull LFS
shell: bash
run: git lfs pull run: git lfs pull
- name: NuGet Install - name: NuGet Install
@ -127,7 +176,7 @@ jobs:
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: | dotnet-version: |
9.0.x 10.0.x
- name: DotNet Build - name: DotNet Build
if: ${{ matrix.options.sdk-preview != true }} if: ${{ matrix.options.sdk-preview != true }}
@ -168,11 +217,8 @@ jobs:
Publish: Publish:
needs: [Build] needs: [Build]
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: (github.event_name == 'push') if: (github.event_name == 'push')
steps: steps:
- name: Git Config - name: Git Config
shell: bash shell: bash
@ -213,4 +259,3 @@ jobs:
run: | run: |
dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json --skip-duplicate 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 dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json --skip-duplicate

28
.github/workflows/code-coverage.yml

@ -4,6 +4,7 @@ on:
schedule: schedule:
# 2AM every Tuesday/Thursday # 2AM every Tuesday/Thursday
- cron: "0 2 * * 2,4" - cron: "0 2 * * 2,4"
jobs: jobs:
Build: Build:
strategy: strategy:
@ -14,15 +15,14 @@ jobs:
runtime: -x64 runtime: -x64
codecov: true codecov: true
runs-on: ${{matrix.options.os}} runs-on: ${{ matrix.options.os }}
steps: steps:
- name: Install libgdi+, which is required for tests running on ubuntu - name: Install libgdi+, which is required for tests running on ubuntu
if: ${{ contains(matrix.options.os, 'ubuntu') }} if: ${{ contains(matrix.options.os, 'ubuntu') }}
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev
- name: Git Config - name: Git Config
shell: bash shell: bash
@ -36,16 +36,21 @@ jobs:
fetch-depth: 0 fetch-depth: 0
submodules: recursive submodules: recursive
# See https://github.com/actions/checkout/issues/165#issuecomment-657673315 # Deterministic list of LFS object IDs, then compute a portable key:
- name: Git Create LFS FileList # - `git lfs ls-files -l` lists all tracked LFS objects with their SHA-256
run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id # - `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 - name: Git Setup LFS Cache
uses: actions/cache@v4 uses: actions/cache@v4
id: lfs-cache id: lfs-cache
with: with:
path: .git/lfs path: .git/lfs
key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1 key: lfs-${{ hashFiles('.lfs-assets-id') }}-v1
- name: Git Pull LFS - name: Git Pull LFS
run: git lfs pull run: git lfs pull
@ -69,13 +74,13 @@ jobs:
- name: DotNet Build - name: DotNet Build
shell: pwsh shell: pwsh
run: ./ci-build.ps1 "${{matrix.options.framework}}" run: ./ci-build.ps1 "${{ matrix.options.framework }}"
env: env:
SIXLABORS_TESTING: True SIXLABORS_TESTING: True
- name: DotNet Test - name: DotNet Test
shell: pwsh 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: env:
SIXLABORS_TESTING: True SIXLABORS_TESTING: True
XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit
@ -88,7 +93,8 @@ jobs:
path: tests/Images/ActualOutput/ path: tests/Images/ActualOutput/
- name: Codecov Update - name: Codecov Update
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v5
if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors') if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors')
with: with:
flags: unittests flags: unittests
token: ${{ secrets.CODECOV_TOKEN }}

6
Directory.Build.props

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

2
shared-infrastructure

@ -1 +1 @@
Subproject commit 5e13cde851a3d6e95d0dfdde2a57071f1efda9c3 Subproject commit 57699ffb797bc2389c5d6cbb3b1800f2eb5fb947

4
src/ImageSharp/Advanced/ParallelRowIterator.Wrappers.cs

@ -139,7 +139,7 @@ public static partial class ParallelRowIterator
} }
int yMax = Math.Min(yMin + this.stepY, this.maxY); int yMax = Math.Min(yMin + this.stepY, this.maxY);
var rows = new RowInterval(yMin, yMax); RowInterval rows = new(yMin, yMax);
// Skip the safety copy when invoking a potentially impure method on a readonly field // Skip the safety copy when invoking a potentially impure method on a readonly field
Unsafe.AsRef(in this.operation).Invoke(in rows); Unsafe.AsRef(in this.operation).Invoke(in rows);
@ -185,7 +185,7 @@ public static partial class ParallelRowIterator
} }
int yMax = Math.Min(yMin + this.stepY, this.maxY); int yMax = Math.Min(yMin + this.stepY, this.maxY);
var rows = new RowInterval(yMin, yMax); RowInterval rows = new(yMin, yMax);
using IMemoryOwner<TBuffer> buffer = this.allocator.Allocate<TBuffer>(this.bufferLength); using IMemoryOwner<TBuffer> buffer = this.allocator.Allocate<TBuffer>(this.bufferLength);

28
src/ImageSharp/Advanced/ParallelRowIterator.cs

@ -26,7 +26,7 @@ public static partial class ParallelRowIterator
public static void IterateRows<T>(Configuration configuration, Rectangle rectangle, in T operation) public static void IterateRows<T>(Configuration configuration, Rectangle rectangle, in T operation)
where T : struct, IRowOperation where T : struct, IRowOperation
{ {
var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration); ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
IterateRows(rectangle, in parallelSettings, in operation); IterateRows(rectangle, in parallelSettings, in operation);
} }
@ -65,8 +65,8 @@ public static partial class ParallelRowIterator
} }
int verticalStep = DivideCeil(rectangle.Height, numOfSteps); int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps }; ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
var wrappingOperation = new RowOperationWrapper<T>(top, bottom, verticalStep, in operation); RowOperationWrapper<T> wrappingOperation = new(top, bottom, verticalStep, in operation);
Parallel.For( Parallel.For(
0, 0,
@ -88,7 +88,7 @@ public static partial class ParallelRowIterator
where T : struct, IRowOperation<TBuffer> where T : struct, IRowOperation<TBuffer>
where TBuffer : unmanaged where TBuffer : unmanaged
{ {
var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration); ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
IterateRows<T, TBuffer>(rectangle, in parallelSettings, in operation); IterateRows<T, TBuffer>(rectangle, in parallelSettings, in operation);
} }
@ -135,8 +135,8 @@ public static partial class ParallelRowIterator
} }
int verticalStep = DivideCeil(height, numOfSteps); int verticalStep = DivideCeil(height, numOfSteps);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps }; ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
var wrappingOperation = new RowOperationWrapper<T, TBuffer>(top, bottom, verticalStep, bufferLength, allocator, in operation); RowOperationWrapper<T, TBuffer> wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
Parallel.For( Parallel.For(
0, 0,
@ -156,7 +156,7 @@ public static partial class ParallelRowIterator
public static void IterateRowIntervals<T>(Configuration configuration, Rectangle rectangle, in T operation) public static void IterateRowIntervals<T>(Configuration configuration, Rectangle rectangle, in T operation)
where T : struct, IRowIntervalOperation where T : struct, IRowIntervalOperation
{ {
var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration); ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
IterateRowIntervals(rectangle, in parallelSettings, in operation); IterateRowIntervals(rectangle, in parallelSettings, in operation);
} }
@ -186,14 +186,14 @@ public static partial class ParallelRowIterator
// Avoid TPL overhead in this trivial case: // Avoid TPL overhead in this trivial case:
if (numOfSteps == 1) if (numOfSteps == 1)
{ {
var rows = new RowInterval(top, bottom); RowInterval rows = new(top, bottom);
Unsafe.AsRef(in operation).Invoke(in rows); Unsafe.AsRef(in operation).Invoke(in rows);
return; return;
} }
int verticalStep = DivideCeil(rectangle.Height, numOfSteps); int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps }; ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
var wrappingOperation = new RowIntervalOperationWrapper<T>(top, bottom, verticalStep, in operation); RowIntervalOperationWrapper<T> wrappingOperation = new(top, bottom, verticalStep, in operation);
Parallel.For( Parallel.For(
0, 0,
@ -215,7 +215,7 @@ public static partial class ParallelRowIterator
where T : struct, IRowIntervalOperation<TBuffer> where T : struct, IRowIntervalOperation<TBuffer>
where TBuffer : unmanaged where TBuffer : unmanaged
{ {
var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration); ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
IterateRowIntervals<T, TBuffer>(rectangle, in parallelSettings, in operation); IterateRowIntervals<T, TBuffer>(rectangle, in parallelSettings, in operation);
} }
@ -250,7 +250,7 @@ public static partial class ParallelRowIterator
// Avoid TPL overhead in this trivial case: // Avoid TPL overhead in this trivial case:
if (numOfSteps == 1) if (numOfSteps == 1)
{ {
var rows = new RowInterval(top, bottom); RowInterval rows = new(top, bottom);
using IMemoryOwner<TBuffer> buffer = allocator.Allocate<TBuffer>(bufferLength); using IMemoryOwner<TBuffer> buffer = allocator.Allocate<TBuffer>(bufferLength);
Unsafe.AsRef(in operation).Invoke(in rows, buffer.Memory.Span); Unsafe.AsRef(in operation).Invoke(in rows, buffer.Memory.Span);
@ -259,8 +259,8 @@ public static partial class ParallelRowIterator
} }
int verticalStep = DivideCeil(height, numOfSteps); int verticalStep = DivideCeil(height, numOfSteps);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps }; ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
var wrappingOperation = new RowIntervalOperationWrapper<T, TBuffer>(top, bottom, verticalStep, bufferLength, allocator, in operation); RowIntervalOperationWrapper<T, TBuffer> wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
Parallel.For( Parallel.For(
0, 0,

8
src/ImageSharp/Color/Color.WebSafePalette.cs

@ -8,15 +8,15 @@ namespace SixLabors.ImageSharp;
/// </content> /// </content>
public partial struct Color public partial struct Color
{ {
private static readonly Lazy<Color[]> WebSafePaletteLazy = new Lazy<Color[]>(CreateWebSafePalette, true); private static readonly Lazy<Color[]> WebSafePaletteLazy = new(CreateWebSafePalette, true);
/// <summary> /// <summary>
/// Gets a collection of named, web safe colors as defined in the CSS Color Module Level 4. /// Gets a collection of named, web safe colors as defined in the CSS Color Module Level 4.
/// </summary> /// </summary>
public static ReadOnlyMemory<Color> WebSafePalette => WebSafePaletteLazy.Value; public static ReadOnlyMemory<Color> WebSafePalette => WebSafePaletteLazy.Value;
private static Color[] CreateWebSafePalette() => new[] private static Color[] CreateWebSafePalette() =>
{ [
AliceBlue, AliceBlue,
AntiqueWhite, AntiqueWhite,
Aqua, Aqua,
@ -159,5 +159,5 @@ public partial struct Color
WhiteSmoke, WhiteSmoke,
Yellow, Yellow,
YellowGreen YellowGreen
}; ];
} }

8
src/ImageSharp/Color/Color.WernerPalette.cs

@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp;
/// </content> /// </content>
public partial struct Color public partial struct Color
{ {
private static readonly Lazy<Color[]> WernerPaletteLazy = new Lazy<Color[]>(CreateWernerPalette, true); private static readonly Lazy<Color[]> WernerPaletteLazy = new(CreateWernerPalette, true);
/// <summary> /// <summary>
/// Gets a collection of colors as defined in the original second edition of Werner’s Nomenclature of Colours 1821. /// Gets a collection of colors as defined in the original second edition of Werner’s Nomenclature of Colours 1821.
@ -16,8 +16,8 @@ public partial struct Color
/// </summary> /// </summary>
public static ReadOnlyMemory<Color> WernerPalette => WernerPaletteLazy.Value; public static ReadOnlyMemory<Color> WernerPalette => WernerPaletteLazy.Value;
private static Color[] CreateWernerPalette() => new[] private static Color[] CreateWernerPalette() =>
{ [
ParseHex("#f1e9cd"), ParseHex("#f1e9cd"),
ParseHex("#f2e7cf"), ParseHex("#f2e7cf"),
ParseHex("#ece6d0"), ParseHex("#ece6d0"),
@ -128,5 +128,5 @@ public partial struct Color
ParseHex("#9b856b"), ParseHex("#9b856b"),
ParseHex("#766051"), ParseHex("#766051"),
ParseHex("#453b32") ParseHex("#453b32")
}; ];
} }

394
src/ImageSharp/Color/Color.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Globalization;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -81,10 +82,10 @@ public readonly partial struct Color : IEquatable<Color>
PixelTypeInfo info = TPixel.GetPixelTypeInfo(); PixelTypeInfo info = TPixel.GetPixelTypeInfo();
if (info.ComponentInfo.HasValue && info.ComponentInfo.Value.GetMaximumComponentPrecision() <= (int)PixelComponentBitDepth.Bit32) if (info.ComponentInfo.HasValue && info.ComponentInfo.Value.GetMaximumComponentPrecision() <= (int)PixelComponentBitDepth.Bit32)
{ {
return new(source.ToScaledVector4()); return new Color(source.ToScaledVector4());
} }
return new(source); return new Color(source);
} }
/// <summary> /// <summary>
@ -95,6 +96,21 @@ public readonly partial struct Color : IEquatable<Color>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Color FromScaledVector(Vector4 source) => new(source); public static Color FromScaledVector(Vector4 source) => new(source);
/// <summary>
/// Bulk converts a span of generic scaled <see cref="Vector4"/> to a span of <see cref="Color"/>.
/// </summary>
/// <param name="source">The source vector span.</param>
/// <param name="destination">The destination color span.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void FromScaledVector(ReadOnlySpan<Vector4> source, Span<Color> destination)
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
for (int i = 0; i < source.Length; i++)
{
destination[i] = FromScaledVector(source[i]);
}
}
/// <summary> /// <summary>
/// Bulk converts a span of a specified <typeparamref name="TPixel"/> type to a span of <see cref="Color"/>. /// Bulk converts a span of a specified <typeparamref name="TPixel"/> type to a span of <see cref="Color"/>.
/// </summary> /// </summary>
@ -111,81 +127,106 @@ public readonly partial struct Color : IEquatable<Color>
PixelTypeInfo info = TPixel.GetPixelTypeInfo(); PixelTypeInfo info = TPixel.GetPixelTypeInfo();
if (info.ComponentInfo.HasValue && info.ComponentInfo.Value.GetMaximumComponentPrecision() <= (int)PixelComponentBitDepth.Bit32) if (info.ComponentInfo.HasValue && info.ComponentInfo.Value.GetMaximumComponentPrecision() <= (int)PixelComponentBitDepth.Bit32)
{ {
for (int i = 0; i < destination.Length; i++) for (int i = 0; i < source.Length; i++)
{ {
destination[i] = FromScaledVector(source[i].ToScaledVector4()); destination[i] = FromScaledVector(source[i].ToScaledVector4());
} }
} }
else else
{ {
for (int i = 0; i < destination.Length; i++) for (int i = 0; i < source.Length; i++)
{ {
destination[i] = new(source[i]); destination[i] = new Color(source[i]);
} }
} }
} }
/// <summary> /// <summary>
/// Creates a new instance of the <see cref="Color"/> struct /// Gets a <see cref="Color"/> from the given hexadecimal string.
/// from the given hexadecimal string.
/// </summary> /// </summary>
/// <param name="hex"> /// <param name="hex">
/// The hexadecimal representation of the combined color components arranged /// The hexadecimal representation of the combined color components.
/// in rgb, rgba, rrggbb, or rrggbbaa format to match web syntax. /// </param>
/// <param name="format">
/// The format of the hexadecimal string to parse, if applicable. Defaults to <see cref="ColorHexFormat.Rgba"/>.
/// </param> /// </param>
/// <returns> /// <returns>
/// The <see cref="Color"/>. /// The <see cref="Color"/> equivalent of the hexadecimal input.
/// </returns> /// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] /// <exception cref="ArgumentException">
public static Color ParseHex(string hex) /// Thrown when the <paramref name="hex"/> is not in the correct format.
/// </exception>
public static Color ParseHex(string hex, ColorHexFormat format = ColorHexFormat.Rgba)
{ {
Rgba32 rgba = Rgba32.ParseHex(hex); Guard.NotNull(hex, nameof(hex));
return FromPixel(rgba);
if (!TryParseHex(hex, out Color color, format))
{
throw new ArgumentException("Hexadecimal string is not in the correct format.", nameof(hex));
}
return color;
} }
/// <summary> /// <summary>
/// Attempts to creates a new instance of the <see cref="Color"/> struct /// Gets a <see cref="Color"/> from the given hexadecimal string.
/// from the given hexadecimal string.
/// </summary> /// </summary>
/// <param name="hex"> /// <param name="hex">
/// The hexadecimal representation of the combined color components arranged /// The hexadecimal representation of the combined color components.
/// in rgb, rgba, rrggbb, or rrggbbaa format to match web syntax. /// </param>
/// <param name="result">
/// When this method returns, contains the <see cref="Color"/> equivalent of the hexadecimal input.
/// </param>
/// <param name="format">
/// The format of the hexadecimal string to parse, if applicable. Defaults to <see cref="ColorHexFormat.Rgba"/>.
/// </param> /// </param>
/// <param name="result">When this method returns, contains the <see cref="Color"/> equivalent of the hexadecimal input.</param>
/// <returns> /// <returns>
/// The <see cref="bool"/>. /// <see langword="true"/> if the parsing was successful; otherwise, <see langword="false"/>.
/// </returns> /// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryParseHex(string hex, out Color result, ColorHexFormat format = ColorHexFormat.Rgba)
public static bool TryParseHex(string hex, out Color result)
{ {
result = default; result = default;
if (Rgba32.TryParseHex(hex, out Rgba32 rgba)) if (format == ColorHexFormat.Argb)
{ {
result = FromPixel(rgba); if (TryParseArgbHex(hex, out Argb32 argb))
return true; {
result = FromPixel(argb);
return true;
}
}
else if (format == ColorHexFormat.Rgba)
{
if (TryParseRgbaHex(hex, out Rgba32 rgba))
{
result = FromPixel(rgba);
return true;
}
} }
return false; return false;
} }
/// <summary> /// <summary>
/// Creates a new instance of the <see cref="Color"/> struct /// Gets a <see cref="Color"/> from the given input string.
/// from the given input string.
/// </summary> /// </summary>
/// <param name="input"> /// <param name="input">
/// The name of the color or the hexadecimal representation of the combined color components arranged /// The name of the color or the hexadecimal representation of the combined color components.
/// in rgb, rgba, rrggbb, or rrggbbaa format to match web syntax. /// </param>
/// <param name="format">
/// The format of the hexadecimal string to parse, if applicable. Defaults to <see cref="ColorHexFormat.Rgba"/>.
/// </param> /// </param>
/// <returns> /// <returns>
/// The <see cref="Color"/>. /// The <see cref="Color"/> equivalent of the input string.
/// </returns> /// </returns>
/// <exception cref="ArgumentException">Input string is not in the correct format.</exception> /// <exception cref="ArgumentException">
public static Color Parse(string input) /// Thrown when the <paramref name="input"/> is not in the correct format.
/// </exception>
public static Color Parse(string input, ColorHexFormat format = ColorHexFormat.Rgba)
{ {
Guard.NotNull(input, nameof(input)); Guard.NotNull(input, nameof(input));
if (!TryParse(input, out Color color)) if (!TryParse(input, out Color color, format))
{ {
throw new ArgumentException("Input string is not in the correct format.", nameof(input)); throw new ArgumentException("Input string is not in the correct format.", nameof(input));
} }
@ -194,18 +235,21 @@ public readonly partial struct Color : IEquatable<Color>
} }
/// <summary> /// <summary>
/// Attempts to creates a new instance of the <see cref="Color"/> struct /// Tries to create a new instance of the <see cref="Color"/> struct from the given input string.
/// from the given input string.
/// </summary> /// </summary>
/// <param name="input"> /// <param name="input">
/// The name of the color or the hexadecimal representation of the combined color components arranged /// The name of the color or the hexadecimal representation of the combined color components.
/// in rgb, rgba, rrggbb, or rrggbbaa format to match web syntax. /// </param>
/// <param name="result">
/// When this method returns, contains the <see cref="Color"/> equivalent of the input string.
/// </param>
/// <param name="format">
/// The format of the hexadecimal string to parse, if applicable. Defaults to <see cref="ColorHexFormat.Rgba"/>.
/// </param> /// </param>
/// <param name="result">When this method returns, contains the <see cref="Color"/> equivalent of the hexadecimal input.</param>
/// <returns> /// <returns>
/// The <see cref="bool"/>. /// <see langword="true"/> if the parsing was successful; otherwise, <see langword="false"/>.
/// </returns> /// </returns>
public static bool TryParse(string input, out Color result) public static bool TryParse(string input, out Color result, ColorHexFormat format = ColorHexFormat.Rgba)
{ {
result = default; result = default;
@ -219,7 +263,13 @@ public readonly partial struct Color : IEquatable<Color>
return true; return true;
} }
return TryParseHex(input, out result); result = default;
if (string.IsNullOrWhiteSpace(input))
{
return false;
}
return TryParseHex(input, out result, format);
} }
/// <summary> /// <summary>
@ -227,6 +277,7 @@ public readonly partial struct Color : IEquatable<Color>
/// </summary> /// </summary>
/// <param name="alpha">The new value of alpha [0..1].</param> /// <param name="alpha">The new value of alpha [0..1].</param>
/// <returns>The color having it's alpha channel altered.</returns> /// <returns>The color having it's alpha channel altered.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Color WithAlpha(float alpha) public Color WithAlpha(float alpha)
{ {
Vector4 v = this.ToScaledVector4(); Vector4 v = this.ToScaledVector4();
@ -235,22 +286,32 @@ public readonly partial struct Color : IEquatable<Color>
} }
/// <summary> /// <summary>
/// Gets the hexadecimal representation of the color instance in rrggbbaa form. /// Gets the hexadecimal string representation of the color instance.
/// </summary> /// </summary>
/// <param name="format">
/// The format of the hexadecimal string to return. Defaults to <see cref="ColorHexFormat.Rgba"/>.
/// </param>
/// <returns>A hexadecimal string representation of the value.</returns> /// <returns>A hexadecimal string representation of the value.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the <paramref name="format"/> is not supported.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public string ToHex() public string ToHex(ColorHexFormat format = ColorHexFormat.Rgba)
{ {
if (this.boxedHighPrecisionPixel is not null) Rgba32 rgba = (this.boxedHighPrecisionPixel is not null)
? this.boxedHighPrecisionPixel.ToRgba32()
: Rgba32.FromScaledVector4(this.data);
uint hexOrder = format switch
{ {
return this.boxedHighPrecisionPixel.ToRgba32().ToHex(); ColorHexFormat.Argb => (uint)((rgba.B << 0) | (rgba.G << 8) | (rgba.R << 16) | (rgba.A << 24)),
} ColorHexFormat.Rgba => (uint)((rgba.A << 0) | (rgba.B << 8) | (rgba.G << 16) | (rgba.R << 24)),
_ => throw new ArgumentOutOfRangeException(nameof(format), format, "Unsupported color hex format.")
};
return Rgba32.FromScaledVector4(this.data).ToHex(); return hexOrder.ToString("X8", CultureInfo.InvariantCulture);
} }
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() => this.ToHex(); public override string ToString() => this.ToHex(ColorHexFormat.Rgba);
/// <summary> /// <summary>
/// Converts the color instance to a specified <typeparamref name="TPixel"/> type. /// Converts the color instance to a specified <typeparamref name="TPixel"/> type.
@ -336,4 +397,241 @@ public readonly partial struct Color : IEquatable<Color>
return this.boxedHighPrecisionPixel.GetHashCode(); return this.boxedHighPrecisionPixel.GetHashCode();
} }
/// <summary>
/// Gets the hexadecimal string representation of the color instance in the format RRGGBBAA.
/// </summary>
/// <param name="hex">
/// The hexadecimal representation of the combined color components.
/// </param>
/// <param name="result">
/// When this method returns, contains the <see cref="Rgba32"/> equivalent of the hexadecimal input.
/// </param>
/// <returns>
/// <see langword="true"/> if the parsing was successful; otherwise, <see langword="false"/>.
/// </returns>
private static bool TryParseRgbaHex(string? hex, out Rgba32 result)
{
result = default;
if (!TryConvertToRgbaUInt32(hex, out uint packedValue))
{
return false;
}
result = Unsafe.As<uint, Rgba32>(ref packedValue);
return true;
}
/// <summary>
/// Gets the hexadecimal string representation of the color instance in the format AARRGGBB.
/// </summary>
/// <param name="hex">
/// The hexadecimal representation of the combined color components.
/// </param>
/// <param name="result">
/// When this method returns, contains the <see cref="Argb32"/> equivalent of the hexadecimal input.
/// </param>
/// <returns>
/// <see langword="true"/> if the parsing was successful; otherwise, <see langword="false"/>.
/// </returns>
private static bool TryParseArgbHex(string? hex, out Argb32 result)
{
result = default;
if (!TryConvertToArgbUInt32(hex, out uint packedValue))
{
return false;
}
result = Unsafe.As<uint, Argb32>(ref packedValue);
return true;
}
private static bool TryConvertToRgbaUInt32(string? value, out uint result)
{
result = default;
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
ReadOnlySpan<char> hex = value.AsSpan();
if (hex[0] == '#')
{
hex = hex[1..];
}
byte a = 255, r, g, b;
switch (hex.Length)
{
case 8:
if (!TryParseByte(hex[0], hex[1], out r) ||
!TryParseByte(hex[2], hex[3], out g) ||
!TryParseByte(hex[4], hex[5], out b) ||
!TryParseByte(hex[6], hex[7], out a))
{
return false;
}
break;
case 6:
if (!TryParseByte(hex[0], hex[1], out r) ||
!TryParseByte(hex[2], hex[3], out g) ||
!TryParseByte(hex[4], hex[5], out b))
{
return false;
}
break;
case 4:
if (!TryExpand(hex[0], out r) ||
!TryExpand(hex[1], out g) ||
!TryExpand(hex[2], out b) ||
!TryExpand(hex[3], out a))
{
return false;
}
break;
case 3:
if (!TryExpand(hex[0], out r) ||
!TryExpand(hex[1], out g) ||
!TryExpand(hex[2], out b))
{
return false;
}
break;
default:
return false;
}
result = (uint)(r | (g << 8) | (b << 16) | (a << 24)); // RGBA layout
return true;
}
private static bool TryConvertToArgbUInt32(string? value, out uint result)
{
result = default;
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
ReadOnlySpan<char> hex = value.AsSpan();
if (hex[0] == '#')
{
hex = hex[1..];
}
byte a = 255, r, g, b;
switch (hex.Length)
{
case 8:
if (!TryParseByte(hex[0], hex[1], out a) ||
!TryParseByte(hex[2], hex[3], out r) ||
!TryParseByte(hex[4], hex[5], out g) ||
!TryParseByte(hex[6], hex[7], out b))
{
return false;
}
break;
case 6:
if (!TryParseByte(hex[0], hex[1], out r) ||
!TryParseByte(hex[2], hex[3], out g) ||
!TryParseByte(hex[4], hex[5], out b))
{
return false;
}
break;
case 4:
if (!TryExpand(hex[0], out a) ||
!TryExpand(hex[1], out r) ||
!TryExpand(hex[2], out g) ||
!TryExpand(hex[3], out b))
{
return false;
}
break;
case 3:
if (!TryExpand(hex[0], out r) ||
!TryExpand(hex[1], out g) ||
!TryExpand(hex[2], out b))
{
return false;
}
break;
default:
return false;
}
result = (uint)((b << 24) | (g << 16) | (r << 8) | a);
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryParseByte(char hi, char lo, out byte value)
{
if (TryConvertHexCharToByte(hi, out byte high) && TryConvertHexCharToByte(lo, out byte low))
{
value = (byte)((high << 4) | low);
return true;
}
value = 0;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryExpand(char c, out byte value)
{
if (TryConvertHexCharToByte(c, out byte nibble))
{
value = (byte)((nibble << 4) | nibble);
return true;
}
value = 0;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryConvertHexCharToByte(char c, out byte value)
{
if ((uint)(c - '0') <= 9)
{
value = (byte)(c - '0');
return true;
}
char lower = (char)(c | 0x20); // Normalize to lowercase
if ((uint)(lower - 'a') <= 5)
{
value = (byte)(lower - 'a' + 10);
return true;
}
value = 0;
return false;
}
} }

40
src/ImageSharp/Color/ColorHexFormat.cs

@ -0,0 +1,40 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp;
/// <summary>
/// Specifies the channel order when formatting or parsing a color as a hexadecimal string.
/// </summary>
public enum ColorHexFormat
{
/// <summary>
/// Uses <c>RRGGBBAA</c> channel order where the red, green, and blue components come first,
/// followed by the alpha component. This matches the CSS Color Module Level 4 and common web standards.
/// <para>
/// When parsing, supports the following formats:
/// <list type="bullet">
/// <item><description><c>#RGB</c> expands to <c>RRGGBBFF</c> (fully opaque)</description></item>
/// <item><description><c>#RGBA</c> expands to <c>RRGGBBAA</c></description></item>
/// <item><description><c>#RRGGBB</c> expands to <c>RRGGBBFF</c> (fully opaque)</description></item>
/// <item><description><c>#RRGGBBAA</c> used as-is</description></item>
/// </list>
/// </para>
/// When formatting, outputs an 8-digit hex string in <c>RRGGBBAA</c> order.
/// </summary>
Rgba,
/// <summary>
/// Uses <c>AARRGGBB</c> channel order where the alpha component comes first,
/// followed by the red, green, and blue components. This matches the Microsoft/XAML convention.
/// <para>
/// When parsing, supports the following formats:
/// <list type="bullet">
/// <item><description><c>#ARGB</c> expands to <c>AARRGGBB</c></description></item>
/// <item><description><c>#AARRGGBB</c> used as-is</description></item>
/// </list>
/// </para>
/// When formatting, outputs an 8-digit hex string in <c>AARRGGBB</c> order.
/// </summary>
Argb
}

2
src/ImageSharp/ColorProfiles/CieLab.cs

@ -182,7 +182,7 @@ public readonly struct CieLab : IProfileConnectingSpace<CieLab, CieXyz>
Vector3 wxyz = new(whitePoint.X, whitePoint.Y, whitePoint.Z); Vector3 wxyz = new(whitePoint.X, whitePoint.Y, whitePoint.Z);
Vector3 xyzr = new(xr, yr, zr); Vector3 xyzr = new(xr, yr, zr);
return new(xyzr * wxyz); return new CieXyz(xyzr * wxyz);
} }
/// <inheritdoc/> /// <inheritdoc/>

2
src/ImageSharp/ColorProfiles/ColorProfileConverter.cs

@ -12,7 +12,7 @@ public class ColorProfileConverter
/// Initializes a new instance of the <see cref="ColorProfileConverter"/> class. /// Initializes a new instance of the <see cref="ColorProfileConverter"/> class.
/// </summary> /// </summary>
public ColorProfileConverter() public ColorProfileConverter()
: this(new()) : this(new ColorConversionOptions())
{ {
} }

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieLab.cs

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

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs

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

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs

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

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs

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

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs

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

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs

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

49
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs

@ -39,6 +39,24 @@ internal static class ColorProfileConverterExtensionsIcc
0.0033717495F, 0.0034852044F, 0.0028800198F, 0F, 0.0033717495F, 0.0034852044F, 0.0028800198F, 0F,
0.0033717495F, 0.0034852044F, 0.0028800198F, 0F]; 0.0033717495F, 0.0034852044F, 0.0028800198F, 0F];
/// <summary>
/// Converts a color value from one ICC color profile to another using the specified color profile converter.
/// </summary>
/// <remarks>
/// This method performs color conversion using ICC profiles, ensuring accurate color mapping
/// between different color spaces. Both the source and target ICC profiles must be provided in the converter's
/// options. The method supports perceptual adjustments when required by the profiles.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo}"/>.</typeparam>
/// <param name="converter">The color profile converter configured with source and target ICC profiles.</param>
/// <param name="source">The color value to convert, defined in the source color profile.</param>
/// <returns>
/// A color value in the target color profile, resulting from the ICC profile-based conversion of the source value.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Thrown if either the source or target ICC profile is missing from the converter options.
/// </exception>
internal static TTo ConvertUsingIccProfile<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source) internal static TTo ConvertUsingIccProfile<TFrom, TTo>(this ColorProfileConverter converter, in TFrom source)
where TFrom : struct, IColorProfile<TFrom> where TFrom : struct, IColorProfile<TFrom>
where TTo : struct, IColorProfile<TTo> where TTo : struct, IColorProfile<TTo>
@ -60,8 +78,8 @@ internal static class ColorProfileConverterExtensionsIcc
ColorProfileConverter pcsConverter = new(new ColorConversionOptions ColorProfileConverter pcsConverter = new(new ColorConversionOptions
{ {
MemoryAllocator = converter.Options.MemoryAllocator, MemoryAllocator = converter.Options.MemoryAllocator,
SourceWhitePoint = new CieXyz(converter.Options.SourceIccProfile.Header.PcsIlluminant), SourceWhitePoint = KnownIlluminants.D50Icc,
TargetWhitePoint = new CieXyz(converter.Options.TargetIccProfile.Header.PcsIlluminant), TargetWhitePoint = KnownIlluminants.D50Icc
}); });
// Normalize the source, then convert to the PCS space. // Normalize the source, then convert to the PCS space.
@ -81,6 +99,29 @@ internal static class ColorProfileConverterExtensionsIcc
return TTo.FromScaledVector4(targetParams.Converter.Calculate(targetPcs)); return TTo.FromScaledVector4(targetParams.Converter.Calculate(targetPcs));
} }
/// <summary>
/// Converts a span of color values from a source color profile to a destination color profile using ICC profiles.
/// </summary>
/// <remarks>
/// This method performs color conversion by transforming the input values through the Profile
/// Connection Space (PCS) as defined by the provided ICC profiles. Perceptual adjustments are applied as required
/// by the profiles. The method does not support absolute colorimetric intent and will not perform such
/// conversions.
/// </remarks>
/// <typeparam name="TFrom">The type representing the source color profile. Must implement <see cref="IColorProfile{TFrom}"/>.</typeparam>
/// <typeparam name="TTo">The type representing the destination color profile. Must implement <see cref="IColorProfile{TTo}"/>.</typeparam>
/// <param name="converter">The color profile converter that provides conversion options and ICC profiles.</param>
/// <param name="source">
/// A read-only span containing the source color values to convert. The values must conform to the source color
/// profile.
/// </param>
/// <param name="destination">
/// A span to receive the converted color values in the destination color profile. Must be at least as large as the
/// source span.
/// </param>
/// <exception cref="InvalidOperationException">
/// Thrown if the source or target ICC profile is missing from the converter options.
/// </exception>
internal static void ConvertUsingIccProfile<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination) internal static void ConvertUsingIccProfile<TFrom, TTo>(this ColorProfileConverter converter, ReadOnlySpan<TFrom> source, Span<TTo> destination)
where TFrom : struct, IColorProfile<TFrom> where TFrom : struct, IColorProfile<TFrom>
where TTo : struct, IColorProfile<TTo> where TTo : struct, IColorProfile<TTo>
@ -104,8 +145,8 @@ internal static class ColorProfileConverterExtensionsIcc
ColorProfileConverter pcsConverter = new(new ColorConversionOptions ColorProfileConverter pcsConverter = new(new ColorConversionOptions
{ {
MemoryAllocator = converter.Options.MemoryAllocator, MemoryAllocator = converter.Options.MemoryAllocator,
SourceWhitePoint = new CieXyz(converter.Options.SourceIccProfile.Header.PcsIlluminant), SourceWhitePoint = KnownIlluminants.D50Icc,
TargetWhitePoint = new CieXyz(converter.Options.TargetIccProfile.Header.PcsIlluminant), TargetWhitePoint = KnownIlluminants.D50Icc
}); });
using IMemoryOwner<Vector4> pcsBuffer = converter.Options.MemoryAllocator.Allocate<Vector4>(source.Length); using IMemoryOwner<Vector4> pcsBuffer = converter.Options.MemoryAllocator.Allocate<Vector4>(source.Length);

192
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs

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

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs

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

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs

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

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs

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

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

@ -58,7 +58,7 @@ internal class ColorTrcCalculator : IVector4Calculator
// when data to PCS, upstream process provides scaled XYZ // when data to PCS, upstream process provides scaled XYZ
// but input to calculator is descaled XYZ // but input to calculator is descaled XYZ
// (see DemoMaxICC IccCmm.cpp : CIccXformMatrixTRC::Apply) // (see DemoMaxICC IccCmm.cpp : CIccXformMatrixTRC::Apply)
xyz = new(CieXyz.FromScaledVector4(xyz).AsVector3Unsafe(), 1); xyz = new Vector4(CieXyz.FromScaledVector4(xyz).AsVector3Unsafe(), 1);
return this.curveCalculator.Calculate(xyz); return this.curveCalculator.Calculate(xyz);
} }
} }

8
src/ImageSharp/ColorProfiles/KnownIlluminants.cs

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

184
src/ImageSharp/ColorProfiles/Rgb.cs

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

2
src/ImageSharp/ColorProfiles/VonKriesChromaticAdaptation.cs

@ -31,7 +31,7 @@ public static class VonKriesChromaticAdaptation
if (from.Equals(to)) if (from.Equals(to))
{ {
return new(source.X, source.Y, source.Z); return new CieXyz(source.X, source.Y, source.Z);
} }
Vector3 sourceColorLms = Vector3.Transform(source.AsVector3Unsafe(), matrix); Vector3 sourceColorLms = Vector3.Transform(source.AsVector3Unsafe(), matrix);

2
src/ImageSharp/ColorProfiles/Y.cs

@ -92,7 +92,7 @@ public readonly struct Y : IColorProfile<Y, Rgb>
{ {
Matrix4x4 m = options.YCbCrTransform.Forward; Matrix4x4 m = options.YCbCrTransform.Forward;
float offset = options.YCbCrTransform.Offset.X; float offset = options.YCbCrTransform.Offset.X;
return new(Vector3.Dot(source.AsVector3Unsafe(), new Vector3(m.M11, m.M12, m.M13)) + offset); return new Y(Vector3.Dot(source.AsVector3Unsafe(), new Vector3(m.M11, m.M12, m.M13)) + offset);
} }
/// <inheritdoc/> /// <inheritdoc/>

8
src/ImageSharp/Common/Helpers/HexConverter.cs

@ -35,8 +35,8 @@ internal static class HexConverter
{ {
// Map from an ASCII char to its hex value, e.g. arr['b'] == 11. 0xFF means it's not a hex digit. // Map from an ASCII char to its hex value, e.g. arr['b'] == 11. 0xFF means it's not a hex digit.
// This doesn't actually allocate. // This doesn't actually allocate.
ReadOnlySpan<byte> charToHexLookup = new byte[] ReadOnlySpan<byte> charToHexLookup =
{ [
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47
@ -52,8 +52,8 @@ internal static class HexConverter
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 255 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 255
}; ];
return (uint)c >= (uint)charToHexLookup.Length ? 0xFF : charToHexLookup[c]; return (uint)c >= (uint)charToHexLookup.Length ? 0xFF : charToHexLookup[c];
} }

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

@ -470,8 +470,8 @@ internal static class Numerics
where T : unmanaged where T : unmanaged
{ {
ref T sRef = ref MemoryMarshal.GetReference(span); ref T sRef = ref MemoryMarshal.GetReference(span);
var vmin = new Vector<T>(min); Vector<T> vmin = new(min);
var vmax = new Vector<T>(max); Vector<T> vmax = new(max);
nint n = (nint)(uint)span.Length / Vector<T>.Count; nint n = (nint)(uint)span.Length / Vector<T>.Count;
nint m = Modulo4(n); nint m = Modulo4(n);
@ -656,7 +656,7 @@ internal static class Numerics
return Sse.Shuffle(value.AsVector128(), value.AsVector128(), ShuffleAlphaControl).AsVector4(); return Sse.Shuffle(value.AsVector128(), value.AsVector128(), ShuffleAlphaControl).AsVector4();
} }
return new(value.W); return new Vector4(value.W);
} }
/// <summary> /// <summary>
@ -726,12 +726,12 @@ internal static class Numerics
ref Vector128<float> vectors128Ref = ref Unsafe.As<Vector4, Vector128<float>>(ref MemoryMarshal.GetReference(vectors)); ref Vector128<float> vectors128Ref = ref Unsafe.As<Vector4, Vector128<float>>(ref MemoryMarshal.GetReference(vectors));
ref Vector128<float> vectors128End = ref Unsafe.Add(ref vectors128Ref, (uint)vectors.Length); ref Vector128<float> vectors128End = ref Unsafe.Add(ref vectors128Ref, (uint)vectors.Length);
var v128_341 = Vector128.Create(341); Vector128<int> v128_341 = Vector128.Create(341);
Vector128<int> v128_negativeZero = Vector128.Create(-0.0f).AsInt32(); Vector128<int> v128_negativeZero = Vector128.Create(-0.0f).AsInt32();
Vector128<int> v128_one = Vector128.Create(1.0f).AsInt32(); Vector128<int> v128_one = Vector128.Create(1.0f).AsInt32();
var v128_13rd = Vector128.Create(1 / 3f); Vector128<float> v128_13rd = Vector128.Create(1 / 3f);
var v128_23rds = Vector128.Create(2 / 3f); Vector128<float> v128_23rds = Vector128.Create(2 / 3f);
while (Unsafe.IsAddressLessThan(ref vectors128Ref, ref vectors128End)) while (Unsafe.IsAddressLessThan(ref vectors128Ref, ref vectors128End))
{ {

2
src/ImageSharp/Common/Helpers/TolerantMath.cs

@ -20,7 +20,7 @@ internal readonly struct TolerantMath
/// It is a field so it can be passed as an 'in' parameter. /// It is a field so it can be passed as an 'in' parameter.
/// Does not necessarily fit all use cases! /// Does not necessarily fit all use cases!
/// </summary> /// </summary>
public static readonly TolerantMath Default = new TolerantMath(1e-8); public static readonly TolerantMath Default = new(1e-8);
public TolerantMath(double epsilon) public TolerantMath(double epsilon)
{ {

2
src/ImageSharp/Common/InlineArray.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
// <auto-generated /> // <auto-generated />

2
src/ImageSharp/Common/InlineArray.tt

@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp;
<#GenerateInlineArrays();#> <#GenerateInlineArrays();#>
<#+ <#+
private static int[] Lengths = new int[] {4, 8, 16 }; private static int[] Lengths = [4, 8, 16 ];
void GenerateInlineArrays() void GenerateInlineArrays()
{ {

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

@ -32,11 +32,11 @@ internal static class Adler32
private const int BlockSize = 1 << 5; private const int BlockSize = 1 << 5;
// The C# compiler emits this as a compile-time constant embedded in the PE file. // The C# compiler emits this as a compile-time constant embedded in the PE file.
private static ReadOnlySpan<byte> Tap1Tap2 => new byte[] private static ReadOnlySpan<byte> Tap1Tap2 =>
{ [
32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, // tap1 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, // tap1
16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 // tap2 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 // tap2
}; ];
/// <summary> /// <summary>
/// Calculates the Adler32 checksum with the bytes taken from the span. /// Calculates the Adler32 checksum with the bytes taken from the span.

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

@ -124,25 +124,25 @@ internal static class DeflaterConstants
/// <summary> /// <summary>
/// Internal compression engine constant /// Internal compression engine constant
/// </summary> /// </summary>
public static int[] GOOD_LENGTH = { 0, 4, 4, 4, 4, 8, 8, 8, 32, 32 }; public static int[] GOOD_LENGTH = [0, 4, 4, 4, 4, 8, 8, 8, 32, 32];
/// <summary> /// <summary>
/// Internal compression engine constant /// Internal compression engine constant
/// </summary> /// </summary>
public static int[] MAX_LAZY = { 0, 4, 5, 6, 4, 16, 16, 32, 128, 258 }; public static int[] MAX_LAZY = [0, 4, 5, 6, 4, 16, 16, 32, 128, 258];
/// <summary> /// <summary>
/// Internal compression engine constant /// Internal compression engine constant
/// </summary> /// </summary>
public static int[] NICE_LENGTH = { 0, 8, 16, 32, 16, 32, 128, 128, 258, 258 }; public static int[] NICE_LENGTH = [0, 8, 16, 32, 16, 32, 128, 128, 258, 258];
/// <summary> /// <summary>
/// Internal compression engine constant /// Internal compression engine constant
/// </summary> /// </summary>
public static int[] MAX_CHAIN = { 0, 4, 8, 32, 16, 32, 128, 256, 1024, 4096 }; public static int[] MAX_CHAIN = [0, 4, 8, 32, 16, 32, 128, 256, 1024, 4096];
/// <summary> /// <summary>
/// Internal compression engine constant /// Internal compression engine constant
/// </summary> /// </summary>
public static int[] COMPR_FUNC = { 0, 1, 1, 1, 1, 2, 2, 2, 2, 2 }; public static int[] COMPR_FUNC = [0, 1, 1, 1, 1, 2, 2, 2, 2, 2];
} }

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

@ -77,8 +77,8 @@ internal sealed unsafe class DeflaterHuffman : IDisposable
// See RFC 1951 3.2.6 // See RFC 1951 3.2.6
// Literal codes // Literal codes
private static readonly short[] StaticLCodes = new short[] private static readonly short[] StaticLCodes =
{ [
12, 140, 76, 204, 44, 172, 108, 236, 28, 156, 92, 220, 60, 188, 124, 252, 12, 140, 76, 204, 44, 172, 108, 236, 28, 156, 92, 220, 60, 188, 124, 252,
2, 130, 66, 194, 34, 162, 98, 226, 18, 146, 82, 210, 50, 178, 114, 242, 2, 130, 66, 194, 34, 162, 98, 226, 18, 146, 82, 210, 50, 178, 114, 242,
10, 138, 74, 202, 42, 170, 106, 234, 26, 154, 90, 218, 58, 186, 122, 250, 10, 138, 74, 202, 42, 170, 106, 234, 26, 154, 90, 218, 58, 186, 122, 250,
@ -97,10 +97,10 @@ internal sealed unsafe class DeflaterHuffman : IDisposable
31, 287, 159, 415, 95, 351, 223, 479, 63, 319, 191, 447, 127, 383, 255, 511, 31, 287, 159, 415, 95, 351, 223, 479, 63, 319, 191, 447, 127, 383, 255, 511,
0, 64, 32, 96, 16, 80, 48, 112, 8, 72, 40, 104, 24, 88, 56, 120, 4, 68, 36, 0, 64, 32, 96, 16, 80, 48, 112, 8, 72, 40, 104, 24, 88, 56, 120, 4, 68, 36,
100, 20, 84, 52, 116, 3, 131, 67, 195, 35, 163 100, 20, 84, 52, 116, 3, 131, 67, 195, 35, 163
}; ];
private static ReadOnlySpan<byte> StaticLLength => new byte[] private static ReadOnlySpan<byte> StaticLLength =>
{ [
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
@ -119,34 +119,34 @@ internal sealed unsafe class DeflaterHuffman : IDisposable
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8
}; ];
// Distance codes and lengths. // Distance codes and lengths.
private static readonly short[] StaticDCodes = new short[] private static readonly short[] StaticDCodes =
{ [
0, 16, 8, 24, 4, 20, 12, 28, 2, 18, 10, 26, 6, 22, 14, 0, 16, 8, 24, 4, 20, 12, 28, 2, 18, 10, 26, 6, 22, 14,
30, 1, 17, 9, 25, 5, 21, 13, 29, 3, 19, 11, 27, 7, 23 30, 1, 17, 9, 25, 5, 21, 13, 29, 3, 19, 11, 27, 7, 23
}; ];
private static ReadOnlySpan<byte> StaticDLength => new byte[] private static ReadOnlySpan<byte> StaticDLength =>
{ [
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5
}; ];
#pragma warning restore SA1201 // Elements should appear in the correct order #pragma warning restore SA1201 // Elements should appear in the correct order
/// <summary> /// <summary>
/// Gets the lengths of the bit length codes are sent in order of decreasing probability, to avoid transmitting the lengths for unused bit length codes. /// Gets the lengths of the bit length codes are sent in order of decreasing probability, to avoid transmitting the lengths for unused bit length codes.
/// </summary> /// </summary>
private static ReadOnlySpan<byte> BitLengthOrder => new byte[] private static ReadOnlySpan<byte> BitLengthOrder =>
{ [
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15
}; ];
private static ReadOnlySpan<byte> Bit4Reverse => new byte[] private static ReadOnlySpan<byte> Bit4Reverse =>
{ [
0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15
}; ];
/// <summary> /// <summary>
/// Gets the pending buffer to use. /// Gets the pending buffer to use.

2
src/ImageSharp/Configuration.cs

@ -122,7 +122,7 @@ public sealed class Configuration
/// <summary> /// <summary>
/// Gets or the <see cref="ImageFormatManager"/> that is currently in use. /// Gets or the <see cref="ImageFormatManager"/> that is currently in use.
/// </summary> /// </summary>
public ImageFormatManager ImageFormatsManager { get; private set; } = new ImageFormatManager(); public ImageFormatManager ImageFormatsManager { get; private set; } = new();
/// <summary> /// <summary>
/// Gets or sets the <see cref="Memory.MemoryAllocator"/> that is currently in use. /// Gets or sets the <see cref="Memory.MemoryAllocator"/> that is currently in use.

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

@ -11,17 +11,17 @@ internal static class BmpConstants
/// <summary> /// <summary>
/// The list of mimetypes that equate to a bmp. /// The list of mimetypes that equate to a bmp.
/// </summary> /// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] public static readonly IEnumerable<string> MimeTypes =
{ [
"image/bmp", "image/bmp",
"image/x-windows-bmp", "image/x-windows-bmp",
"image/x-win-bitmap" "image/x-win-bitmap"
}; ];
/// <summary> /// <summary>
/// The list of file extensions that equate to a bmp. /// The list of file extensions that equate to a bmp.
/// </summary> /// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "bm", "bmp", "dip" }; public static readonly IEnumerable<string> FileExtensions = ["bm", "bmp", "dip"];
/// <summary> /// <summary>
/// Valid magic bytes markers identifying a Bitmap file. /// Valid magic bytes markers identifying a Bitmap file.

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

@ -25,7 +25,7 @@ public sealed class BmpDecoder : SpecializedImageDecoder<BmpDecoderOptions>
Guard.NotNull(options, nameof(options)); Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
return new BmpDecoderCore(new() { GeneralOptions = options }).Identify(options.Configuration, stream, cancellationToken); return new BmpDecoderCore(new BmpDecoderOptions { GeneralOptions = options }).Identify(options.Configuration, stream, cancellationToken);
} }
/// <inheritdoc/> /// <inheritdoc/>

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

@ -220,7 +220,7 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{ {
this.ReadImageHeaders(stream, out _, out _); this.ReadImageHeaders(stream, out _, out _);
return new ImageInfo(new(this.infoHeader.Width, this.infoHeader.Height), this.metadata); return new ImageInfo(new Size(this.infoHeader.Width, this.infoHeader.Height), this.metadata);
} }
/// <summary> /// <summary>
@ -1270,7 +1270,7 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
byte g = (byte)((temp & greenMask) >> rightShiftGreenMask); byte g = (byte)((temp & greenMask) >> rightShiftGreenMask);
byte b = (byte)((temp & blueMask) >> rightShiftBlueMask); byte b = (byte)((temp & blueMask) >> rightShiftBlueMask);
byte a = alphaMask != 0 ? (byte)((temp & alphaMask) >> rightShiftAlphaMask) : byte.MaxValue; byte a = alphaMask != 0 ? (byte)((temp & alphaMask) >> rightShiftAlphaMask) : byte.MaxValue;
pixelRow[x] = TPixel.FromRgba32(new(r, g, b, a)); pixelRow[x] = TPixel.FromRgba32(new Rgba32(r, g, b, a));
} }
offset += 4; offset += 4;
@ -1457,7 +1457,7 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
this.bmpMetadata.InfoHeaderType = infoHeaderType; this.bmpMetadata.InfoHeaderType = infoHeaderType;
this.bmpMetadata.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel; this.bmpMetadata.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel;
this.Dimensions = new(this.infoHeader.Width, this.infoHeader.Height); this.Dimensions = new Size(this.infoHeader.Width, this.infoHeader.Height);
} }
/// <summary> /// <summary>

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

@ -655,7 +655,7 @@ internal sealed class BmpEncoderCore
CancellationToken cancellationToken) CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions() using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions
{ {
MaxColors = 16, MaxColors = 16,
Dither = this.quantizer.Options.Dither, Dither = this.quantizer.Options.Dither,
@ -712,7 +712,7 @@ internal sealed class BmpEncoderCore
CancellationToken cancellationToken) CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions() using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions
{ {
MaxColors = 4, MaxColors = 4,
Dither = this.quantizer.Options.Dither, Dither = this.quantizer.Options.Dither,
@ -778,7 +778,7 @@ internal sealed class BmpEncoderCore
CancellationToken cancellationToken) CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions() using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions
{ {
MaxColors = 2, MaxColors = 2,
Dither = this.quantizer.Options.Dither, Dither = this.quantizer.Options.Dither,

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

@ -15,7 +15,7 @@ public sealed class BmpFormat : IImageFormat<BmpMetadata>
/// <summary> /// <summary>
/// Gets the shared instance. /// Gets the shared instance.
/// </summary> /// </summary>
public static BmpFormat Instance { get; } = new BmpFormat(); public static BmpFormat Instance { get; } = new();
/// <inheritdoc/> /// <inheritdoc/>
public string Name => "BMP"; public string Name => "BMP";

3
src/ImageSharp/Formats/Bmp/BmpMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
// TODO: Add color table information. // TODO: Add color table information.
@ -156,7 +157,7 @@ public class BmpMetadata : IFormatMetadata<BmpMetadata>
public BmpMetadata DeepClone() => new(this); public BmpMetadata DeepClone() => new(this);
/// <inheritdoc/> /// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination) public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
=> this.ColorTable = null; => this.ColorTable = null;
} }

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

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Icon; using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -117,7 +118,7 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
}; };
/// <inheritdoc/> /// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination) public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
float ratioX = destination.Width / (float)source.Width; float ratioX = destination.Width / (float)source.Width;
@ -147,7 +148,7 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
? (byte)0 ? (byte)0
: (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel); : (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel);
return new() return new IconDirEntry
{ {
Width = ClampEncodingDimension(this.EncodingWidth ?? size.Width), Width = ClampEncodingDimension(this.EncodingWidth ?? size.Width),
Height = ClampEncodingDimension(this.EncodingHeight ?? size.Height), Height = ClampEncodingDimension(this.EncodingHeight ?? size.Height),

3
src/ImageSharp/Formats/Cur/CurMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Icon; using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -148,7 +149,7 @@ public class CurMetadata : IFormatMetadata<CurMetadata>
}; };
/// <inheritdoc/> /// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination) public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
=> this.ColorTable = null; => this.ColorTable = null;

10
src/ImageSharp/Formats/DecoderOptions.cs

@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Formats;
/// </summary> /// </summary>
public sealed class DecoderOptions public sealed class DecoderOptions
{ {
private static readonly Lazy<DecoderOptions> LazyOptions = new(() => new()); private static readonly Lazy<DecoderOptions> LazyOptions = new(() => new DecoderOptions());
private uint maxFrames = int.MaxValue; private uint maxFrames = int.MaxValue;
@ -78,12 +78,12 @@ public sealed class DecoderOptions
return false; return false;
} }
if (IccProfileHeader.IsLikelySrgb(profile.Header)) if (this.ColorProfileHandling == ColorProfileHandling.Preserve)
{ {
return false; return false;
} }
if (this.ColorProfileHandling == ColorProfileHandling.Preserve) if (profile.IsCanonicalSrgbMatrixTrc())
{ {
return false; return false;
} }
@ -99,11 +99,11 @@ public sealed class DecoderOptions
return false; return false;
} }
if (this.ColorProfileHandling == ColorProfileHandling.Compact && IccProfileHeader.IsLikelySrgb(profile.Header)) if (this.ColorProfileHandling == ColorProfileHandling.Convert)
{ {
return true; return true;
} }
return this.ColorProfileHandling == ColorProfileHandling.Convert; return this.ColorProfileHandling == ColorProfileHandling.Compact && profile.IsCanonicalSrgbMatrixTrc();
} }
} }

22
src/ImageSharp/Formats/Gif/GifConstants.cs

@ -93,41 +93,41 @@ internal static class GifConstants
/// <summary> /// <summary>
/// The collection of mimetypes that equate to a Gif. /// The collection of mimetypes that equate to a Gif.
/// </summary> /// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] { "image/gif" }; public static readonly IEnumerable<string> MimeTypes = ["image/gif"];
/// <summary> /// <summary>
/// The collection of file extensions that equate to a Gif. /// The collection of file extensions that equate to a Gif.
/// </summary> /// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "gif" }; public static readonly IEnumerable<string> FileExtensions = ["gif"];
/// <summary> /// <summary>
/// Gets the ASCII encoded bytes used to identify the GIF file (combining <see cref="FileType"/> and <see cref="FileVersion"/>). /// Gets the ASCII encoded bytes used to identify the GIF file (combining <see cref="FileType"/> and <see cref="FileVersion"/>).
/// </summary> /// </summary>
internal static ReadOnlySpan<byte> MagicNumber => new[] internal static ReadOnlySpan<byte> MagicNumber =>
{ [
(byte)'G', (byte)'I', (byte)'F', (byte)'G', (byte)'I', (byte)'F',
(byte)'8', (byte)'9', (byte)'a' (byte)'8', (byte)'9', (byte)'a'
}; ];
/// <summary> /// <summary>
/// Gets the ASCII encoded application identification bytes (representing <see cref="NetscapeApplicationIdentification"/>). /// Gets the ASCII encoded application identification bytes (representing <see cref="NetscapeApplicationIdentification"/>).
/// </summary> /// </summary>
internal static ReadOnlySpan<byte> NetscapeApplicationIdentificationBytes => new[] internal static ReadOnlySpan<byte> NetscapeApplicationIdentificationBytes =>
{ [
(byte)'N', (byte)'E', (byte)'T', (byte)'N', (byte)'E', (byte)'T',
(byte)'S', (byte)'C', (byte)'A', (byte)'S', (byte)'C', (byte)'A',
(byte)'P', (byte)'E', (byte)'P', (byte)'E',
(byte)'2', (byte)'.', (byte)'0' (byte)'2', (byte)'.', (byte)'0'
}; ];
/// <summary> /// <summary>
/// Gets the ASCII encoded application identification bytes. /// Gets the ASCII encoded application identification bytes.
/// </summary> /// </summary>
internal static ReadOnlySpan<byte> XmpApplicationIdentificationBytes => new[] internal static ReadOnlySpan<byte> XmpApplicationIdentificationBytes =>
{ [
(byte)'X', (byte)'M', (byte)'P', (byte)'X', (byte)'M', (byte)'P',
(byte)' ', (byte)'D', (byte)'a', (byte)' ', (byte)'D', (byte)'a',
(byte)'t', (byte)'a', (byte)'t', (byte)'a',
(byte)'X', (byte)'M', (byte)'P' (byte)'X', (byte)'M', (byte)'P'
}; ];
} }

84
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -267,7 +267,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore
} }
return new ImageInfo( return new ImageInfo(
new(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height), new Size(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height),
this.metadata, this.metadata,
framesMetadata); framesMetadata);
} }
@ -305,7 +305,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore
GifThrowHelper.ThrowInvalidImageContentException("Width or height should not be 0"); GifThrowHelper.ThrowInvalidImageContentException("Width or height should not be 0");
} }
this.Dimensions = new(this.imageDescriptor.Width, this.imageDescriptor.Height); this.Dimensions = new Size(this.imageDescriptor.Width, this.imageDescriptor.Height);
} }
/// <summary> /// <summary>
@ -414,6 +414,11 @@ internal sealed class GifDecoderCore : ImageDecoderCore
GifThrowHelper.ThrowInvalidImageContentException($"Gif comment length '{length}' exceeds max '{GifConstants.MaxCommentSubBlockLength}' of a comment data block"); GifThrowHelper.ThrowInvalidImageContentException($"Gif comment length '{length}' exceeds max '{GifConstants.MaxCommentSubBlockLength}' of a comment data block");
} }
if (length == -1)
{
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif comment");
}
if (this.skipMetadata) if (this.skipMetadata)
{ {
stream.Seek(length, SeekOrigin.Current); stream.Seek(length, SeekOrigin.Current);
@ -484,6 +489,12 @@ internal sealed class GifDecoderCore : ImageDecoderCore
backgroundColor = Color.Transparent; backgroundColor = Color.Transparent;
} }
// We zero the alpha only when this frame declares transparency so that
// frames with a transparent index coalesce over a transparent canvas rather than
// baking the LSD background as a matte. When the flag is not set, this frame will
// write an opaque color for every addressed pixel; keeping the LSD background
// opaque here allows ReadFrameColors to show that background in uncovered areas
// for non-transparent GIFs that rely on it. We still do not prefill the canvas here.
if (this.graphicsControlExtension.TransparencyFlag) if (this.graphicsControlExtension.TransparencyFlag)
{ {
backgroundColor = backgroundColor.WithAlpha(0); backgroundColor = backgroundColor.WithAlpha(0);
@ -493,24 +504,18 @@ internal sealed class GifDecoderCore : ImageDecoderCore
this.ReadFrameColors(stream, ref image, ref previousFrame, ref previousDisposalMode, colorTable, backgroundColor.ToPixel<TPixel>()); this.ReadFrameColors(stream, ref image, ref previousFrame, ref previousDisposalMode, colorTable, backgroundColor.ToPixel<TPixel>());
// Update from newly decoded frame. // Update from newly decoded frame.
if (this.graphicsControlExtension.DisposalMethod != FrameDisposalMode.RestoreToPrevious) FrameDisposalMode disposalMethod = this.graphicsControlExtension.DisposalMethod;
if (disposalMethod != FrameDisposalMode.RestoreToPrevious)
{ {
if (this.backgroundColorIndex < colorTable.Length) // Do not key this on the transparency flag. Disposal handling is determined by
{ // the previous frame's disposal, not by whether the current frame declares a transparent
backgroundColor = Color.FromPixel(colorTable[this.backgroundColorIndex]); // index. For editing we carry a transparent background so that RestoreToBackground clears
} // remove pixels to transparent rather than painting an opaque matte. The LSD background
else // color is display advice and should be used only when explicitly flattening or when
{ // rendering with an option to honor it.
backgroundColor = Color.Transparent; backgroundColor = (this.backgroundColorIndex < colorTable.Length)
} ? Color.FromPixel(colorTable[this.backgroundColorIndex]).WithAlpha(0)
: Color.Transparent;
// TODO: I don't understand why this is always set to alpha of zero.
// This should be dependent on the transparency flag of the graphics
// control extension. ImageMagick does the same.
// if (this.graphicsControlExtension.TransparencyFlag)
{
backgroundColor = backgroundColor.WithAlpha(0);
}
} }
// Skip any remaining blocks // Skip any remaining blocks
@ -541,30 +546,45 @@ internal sealed class GifDecoderCore : ImageDecoderCore
GifImageDescriptor descriptor = this.imageDescriptor; GifImageDescriptor descriptor = this.imageDescriptor;
int imageWidth = this.logicalScreenDescriptor.Width; int imageWidth = this.logicalScreenDescriptor.Width;
int imageHeight = this.logicalScreenDescriptor.Height; int imageHeight = this.logicalScreenDescriptor.Height;
bool transFlag = this.graphicsControlExtension.TransparencyFlag; bool useTransparency = this.graphicsControlExtension.TransparencyFlag;
bool useBackground;
FrameDisposalMode disposalMethod = this.graphicsControlExtension.DisposalMethod; FrameDisposalMode disposalMethod = this.graphicsControlExtension.DisposalMethod;
ImageFrame<TPixel> currentFrame; ImageFrame<TPixel> currentFrame;
ImageFrame<TPixel>? restoreFrame = null; ImageFrame<TPixel>? restoreFrame = null;
if (previousFrame is null && previousDisposalMode is null) if (previousFrame is null && previousDisposalMode is null)
{ {
image = transFlag // First frame: prefill with LSD background iff a GCT exists (policy: HonorBackgroundColor).
? new Image<TPixel>(this.configuration, imageWidth, imageHeight, this.metadata) useBackground =
: new Image<TPixel>(this.configuration, imageWidth, imageHeight, backgroundPixel, this.metadata); this.logicalScreenDescriptor.GlobalColorTableFlag
&& disposalMethod == FrameDisposalMode.RestoreToBackground;
image = useBackground
? new Image<TPixel>(this.configuration, imageWidth, imageHeight, backgroundPixel, this.metadata)
: new Image<TPixel>(this.configuration, imageWidth, imageHeight, this.metadata);
this.SetFrameMetadata(image.Frames.RootFrame.Metadata); this.SetFrameMetadata(image.Frames.RootFrame.Metadata);
currentFrame = image.Frames.RootFrame; currentFrame = image.Frames.RootFrame;
} }
else else
{ {
// Subsequent frames: use LSD background iff previous disposal was RestoreToBackground and a GCT exists.
useBackground =
this.logicalScreenDescriptor.GlobalColorTableFlag
&& previousDisposalMode == FrameDisposalMode.RestoreToBackground;
if (previousFrame != null) if (previousFrame != null)
{ {
currentFrame = image!.Frames.AddFrame(previousFrame); currentFrame = image!.Frames.AddFrame(previousFrame);
} }
else else if (useBackground)
{ {
currentFrame = image!.Frames.CreateFrame(backgroundPixel); currentFrame = image!.Frames.CreateFrame(backgroundPixel);
} }
else
{
currentFrame = image!.Frames.CreateFrame();
}
this.SetFrameMetadata(currentFrame.Metadata); this.SetFrameMetadata(currentFrame.Metadata);
@ -575,7 +595,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore
if (previousDisposalMode == FrameDisposalMode.RestoreToBackground) if (previousDisposalMode == FrameDisposalMode.RestoreToBackground)
{ {
this.RestoreToBackground(currentFrame, backgroundPixel, transFlag); this.RestoreToBackground(currentFrame, backgroundPixel, !useBackground);
} }
} }
@ -592,7 +612,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore
if (disposalMethod == FrameDisposalMode.RestoreToBackground) if (disposalMethod == FrameDisposalMode.RestoreToBackground)
{ {
this.restoreArea = Rectangle.Intersect(image.Bounds, new(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height)); this.restoreArea = Rectangle.Intersect(image.Bounds, new Rectangle(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height));
} }
if (colorTable.Length == 0) if (colorTable.Length == 0)
@ -665,12 +685,18 @@ internal sealed class GifDecoderCore : ImageDecoderCore
// Take the descriptorLeft..maxX slice of the row, so the loop can be simplified. // Take the descriptorLeft..maxX slice of the row, so the loop can be simplified.
row = row[descriptorLeft..maxX]; row = row[descriptorLeft..maxX];
if (!transFlag) if (!useTransparency)
{ {
for (int x = 0; x < row.Length; x++) for (int x = 0; x < row.Length; x++)
{ {
int index = indicesRow[x]; int index = indicesRow[x];
index = Numerics.Clamp(index, 0, colorTableMaxIdx);
// Treat any out of bounds values as background.
if (index > colorTableMaxIdx)
{
index = Numerics.Clamp(index, 0, colorTableMaxIdx);
}
row[x] = TPixel.FromRgb24(colorTable[index]); row[x] = TPixel.FromRgb24(colorTable[index]);
} }
} }
@ -681,6 +707,8 @@ internal sealed class GifDecoderCore : ImageDecoderCore
int index = indicesRow[x]; int index = indicesRow[x];
// Treat any out of bounds values as transparent. // Treat any out of bounds values as transparent.
// We explicitly set the pixel to transparent rather than alter the inbound
// color palette.
if (index > colorTableMaxIdx || index == transIndex) if (index > colorTableMaxIdx || index == transIndex)
{ {
continue; continue;

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

@ -113,18 +113,22 @@ internal sealed class GifEncoderCore
TransparentColorMode mode = this.transparentColorMode; TransparentColorMode mode = this.transparentColorMode;
// Create a new quantizer options instance augmenting the transparent color mode to match the encoder. // Create a new quantizer options instance augmenting the transparent color mode to match the encoder.
QuantizerOptions options = (this.encoder.Quantizer?.Options ?? new()).DeepClone(o => o.TransparentColorMode = mode); QuantizerOptions options = (this.encoder.Quantizer?.Options ?? new QuantizerOptions()).DeepClone(o => o.TransparentColorMode = mode);
if (globalQuantizer is null) if (globalQuantizer is null)
{ {
// Is this a gif with color information. If so use that, otherwise use octree. // Is this a gif with color information. If so use that, otherwise use octree.
if (gifMetadata.ColorTableMode == FrameColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0) if (gifMetadata.ColorTableMode == FrameColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0)
{ {
int transparencyIndex = GetTransparentIndex(quantized, frameMetadata); int ti = GetTransparentIndex(quantized, frameMetadata);
if (transparencyIndex >= 0 || gifMetadata.GlobalColorTable.Value.Length < 256) if (ti >= 0 || gifMetadata.GlobalColorTable.Value.Length < 256)
{ {
// We avoid dithering by default to preserve the original colors. // We avoid dithering by default to preserve the original colors.
globalQuantizer = new PaletteQuantizer(gifMetadata.GlobalColorTable.Value, options.DeepClone(o => o.Dither = null)); globalQuantizer = new PaletteQuantizer(
gifMetadata.GlobalColorTable.Value,
options.DeepClone(o => o.Dither = null),
ti,
Color.Transparent);
} }
else else
{ {
@ -173,20 +177,14 @@ internal sealed class GifEncoderCore
WriteHeader(stream); WriteHeader(stream);
// Write the LSD. // Write the LSD.
int derivedTransparencyIndex = GetTransparentIndex(quantized, null); int transparencyIndex = GetTransparentIndex(quantized, null);
if (derivedTransparencyIndex >= 0) if (transparencyIndex >= 0)
{ {
frameMetadata.HasTransparency = true; frameMetadata.HasTransparency = true;
frameMetadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex); frameMetadata.TransparencyIndex = ClampIndex(transparencyIndex);
} }
// TODO: We should be checking the metadata here also I think? byte backgroundIndex = GetBackgroundIndex(quantized, gifMetadata, this.backgroundColor);
if (!TryGetBackgroundIndex(quantized, this.backgroundColor, out byte backgroundIndex))
{
backgroundIndex = derivedTransparencyIndex >= 0
? frameMetadata.TransparencyIndex
: gifMetadata.BackgroundColorIndex;
}
// Get the number of bits. // Get the number of bits.
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length); int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
@ -224,7 +222,7 @@ internal sealed class GifEncoderCore
image, image,
globalQuantizer, globalQuantizer,
globalFrameQuantizer, globalFrameQuantizer,
derivedTransparencyIndex, transparencyIndex,
frameMetadata.DisposalMode, frameMetadata.DisposalMode,
cancellationToken); cancellationToken);
} }
@ -334,15 +332,23 @@ internal sealed class GifEncoderCore
{ {
// Capture any explicit transparency index from the metadata. // Capture any explicit transparency index from the metadata.
// We use it to determine the value to use to replace duplicate pixels. // We use it to determine the value to use to replace duplicate pixels.
int transparencyIndex = metadata.HasTransparency ? metadata.TransparencyIndex : -1; bool useTransparency = metadata.HasTransparency;
int transparencyIndex = useTransparency ? metadata.TransparencyIndex : -1;
ImageFrame<TPixel>? previous = previousDisposalMode == FrameDisposalMode.RestoreToBackground ImageFrame<TPixel>? previous = previousDisposalMode == FrameDisposalMode.RestoreToBackground
? null : ? null :
previousFrame; previousFrame;
Color background = metadata.DisposalMode == FrameDisposalMode.RestoreToBackground // If the previous frame has a value we need to check the disposal mode of that frame
? this.backgroundColor ?? Color.Transparent // to determine if we should use the background color to fill the encoding frame
: Color.Transparent; // when de-duplicating.
FrameDisposalMode disposalMode = previous is null ?
metadata.DisposalMode :
previous.Metadata.GetGifMetadata().DisposalMode;
Color background = !useTransparency && disposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
: Color.Transparent;
// Deduplicate and quantize the frame capturing only required parts. // Deduplicate and quantize the frame capturing only required parts.
(bool difference, Rectangle bounds) = (bool difference, Rectangle bounds) =
@ -491,6 +497,7 @@ internal sealed class GifEncoderCore
return quantized; return quantized;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static byte ClampIndex(int value) => (byte)Numerics.Clamp(value, byte.MinValue, byte.MaxValue); private static byte ClampIndex(int value) => (byte)Numerics.Clamp(value, byte.MinValue, byte.MaxValue);
/// <summary> /// <summary>
@ -531,41 +538,38 @@ internal sealed class GifEncoderCore
/// Returns the index of the background color in the palette. /// Returns the index of the background color in the palette.
/// </summary> /// </summary>
/// <param name="quantized">The current quantized frame.</param> /// <param name="quantized">The current quantized frame.</param>
/// <param name="metadata">The gif metadata</param>
/// <param name="background">The background color to match.</param> /// <param name="background">The background color to match.</param>
/// <param name="index">The index in the palette of the background color.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>The <see cref="bool"/>.</returns> /// <returns>The <see cref="byte"/> index of the background color.</returns>
private static bool TryGetBackgroundIndex<TPixel>( private static byte GetBackgroundIndex<TPixel>(IndexedImageFrame<TPixel>? quantized, GifMetadata metadata, Color? background)
IndexedImageFrame<TPixel>? quantized,
Color? background,
out byte index)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int match = -1; int match = -1;
if (quantized != null && background.HasValue) if (quantized != null)
{ {
TPixel backgroundPixel = background.Value.ToPixel<TPixel>(); if (background.HasValue)
ReadOnlySpan<TPixel> palette = quantized.Palette.Span;
for (int i = 0; i < palette.Length; i++)
{ {
if (!backgroundPixel.Equals(palette[i])) TPixel backgroundPixel = background.Value.ToPixel<TPixel>();
ReadOnlySpan<TPixel> palette = quantized.Palette.Span;
for (int i = 0; i < palette.Length; i++)
{ {
continue; if (!backgroundPixel.Equals(palette[i]))
} {
continue;
}
match = i; match = i;
break; break;
}
}
else if (metadata.BackgroundColorIndex < quantized.Palette.Length)
{
match = metadata.BackgroundColorIndex;
} }
} }
if (match >= 0) return ClampIndex(match);
{
index = (byte)Numerics.Clamp(match, 0, 255);
return true;
}
index = 0;
return false;
} }
/// <summary> /// <summary>

5
src/ImageSharp/Formats/Gif/GifFrameMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Gif; namespace SixLabors.ImageSharp.Formats.Gif;
@ -93,7 +94,7 @@ public class GifFrameMetadata : IFormatFrameMetadata<GifFrameMetadata>
// If the color table is global and frame has no transparency. Consider it 'Source' also. // If the color table is global and frame has no transparency. Consider it 'Source' also.
blendSource |= this.ColorTableMode == FrameColorTableMode.Global && !this.HasTransparency; blendSource |= this.ColorTableMode == FrameColorTableMode.Global && !this.HasTransparency;
return new() return new FormatConnectingFrameMetadata
{ {
ColorTableMode = this.ColorTableMode, ColorTableMode = this.ColorTableMode,
Duration = TimeSpan.FromMilliseconds(this.FrameDelay * 10), Duration = TimeSpan.FromMilliseconds(this.FrameDelay * 10),
@ -103,7 +104,7 @@ public class GifFrameMetadata : IFormatFrameMetadata<GifFrameMetadata>
} }
/// <inheritdoc/> /// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination) public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
=> this.LocalColorTable = null; => this.LocalColorTable = null;

3
src/ImageSharp/Formats/Gif/GifMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Gif; namespace SixLabors.ImageSharp.Formats.Gif;
@ -105,7 +106,7 @@ public class GifMetadata : IFormatMetadata<GifMetadata>
}; };
/// <inheritdoc/> /// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination) public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
=> this.GlobalColorTable = null; => this.GlobalColorTable = null;

4
src/ImageSharp/Formats/Gif/LzwEncoder.cs

@ -47,7 +47,7 @@ internal sealed class LzwEncoder : IDisposable
/// Mask used when shifting pixel values /// Mask used when shifting pixel values
/// </summary> /// </summary>
private static readonly int[] Masks = private static readonly int[] Masks =
{ [
0b0, 0b0,
0b1, 0b1,
0b11, 0b11,
@ -65,7 +65,7 @@ internal sealed class LzwEncoder : IDisposable
0b11111111111111, 0b11111111111111,
0b111111111111111, 0b111111111111111,
0b1111111111111111 0b1111111111111111
}; ];
/// <summary> /// <summary>
/// The maximum number of bits/code. /// The maximum number of bits/code.

2
src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs

@ -34,7 +34,7 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension
// Exclude the "magic trailer", see XMP Specification Part 3, 1.1.2 GIF // Exclude the "magic trailer", see XMP Specification Part 3, 1.1.2 GIF
int xmpLength = xmpBytes.Length - 256; // 257 - unread 0x0 int xmpLength = xmpBytes.Length - 256; // 257 - unread 0x0
byte[] buffer = Array.Empty<byte>(); byte[] buffer = [];
if (xmpLength > 0) if (xmpLength > 0)
{ {
buffer = new byte[xmpLength]; buffer = new byte[xmpLength];

4
src/ImageSharp/Formats/IFormatFrameMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats; namespace SixLabors.ImageSharp.Formats;
@ -22,7 +23,8 @@ public interface IFormatFrameMetadata : IDeepCloneable
/// <typeparam name="TPixel">The type of pixel format.</typeparam> /// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="source">The source image frame.</param> /// <param name="source">The source image frame.</param>
/// <param name="destination">The destination image frame.</param> /// <param name="destination">The destination image frame.</param>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination) /// <param name="matrix">The transformation matrix applied to the image frame.</param>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>; where TPixel : unmanaged, IPixel<TPixel>;
} }

4
src/ImageSharp/Formats/IFormatMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats; namespace SixLabors.ImageSharp.Formats;
@ -27,7 +28,8 @@ public interface IFormatMetadata : IDeepCloneable
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam> /// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="destination">The destination image .</param> /// <param name="destination">The destination image .</param>
public void AfterImageApply<TPixel>(Image<TPixel> destination) /// <param name="matrix">The transformation matrix applied to the image.</param>
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>; where TPixel : unmanaged, IPixel<TPixel>;
} }

5
src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Icon; using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -110,7 +111,7 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
}; };
/// <inheritdoc/> /// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination) public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
float ratioX = destination.Width / (float)source.Width; float ratioX = destination.Width / (float)source.Width;
@ -138,7 +139,7 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
? (byte)0 ? (byte)0
: (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel); : (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel);
return new() return new IconDirEntry
{ {
Width = ClampEncodingDimension(this.EncodingWidth ?? size.Width), Width = ClampEncodingDimension(this.EncodingWidth ?? size.Width),
Height = ClampEncodingDimension(this.EncodingHeight ?? size.Height), Height = ClampEncodingDimension(this.EncodingHeight ?? size.Height),

3
src/ImageSharp/Formats/Ico/IcoMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Icon; using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -148,7 +149,7 @@ public class IcoMetadata : IFormatMetadata<IcoMetadata>
}; };
/// <inheritdoc/> /// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination) public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
=> this.ColorTable = null; => this.ColorTable = null;

12
src/ImageSharp/Formats/Icon/IconDecoderCore.cs

@ -63,7 +63,7 @@ internal abstract class IconDecoderCore : ImageDecoderCore
// Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data // Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data
// which technically allows storing icons with larger than 256 pixels, but such larger sizes are not recommended by Microsoft. // which technically allows storing icons with larger than 256 pixels, but such larger sizes are not recommended by Microsoft.
this.Dimensions = new(Math.Max(this.Dimensions.Width, temp.Size.Width), Math.Max(this.Dimensions.Height, temp.Size.Height)); this.Dimensions = new Size(Math.Max(this.Dimensions.Width, temp.Size.Width), Math.Max(this.Dimensions.Height, temp.Size.Height));
} }
ImageMetadata metadata = new(); ImageMetadata metadata = new();
@ -208,7 +208,7 @@ internal abstract class IconDecoderCore : ImageDecoderCore
// Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data // Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data
// which technically allows storing icons with larger than 256 pixels, but such larger sizes are not recommended by Microsoft. // which technically allows storing icons with larger than 256 pixels, but such larger sizes are not recommended by Microsoft.
this.Dimensions = new(Math.Max(this.Dimensions.Width, frameInfo.Size.Width), Math.Max(this.Dimensions.Height, frameInfo.Size.Height)); this.Dimensions = new Size(Math.Max(this.Dimensions.Width, frameInfo.Size.Width), Math.Max(this.Dimensions.Height, frameInfo.Size.Height));
} }
// Copy the format specific metadata to the image. // Copy the format specific metadata to the image.
@ -222,7 +222,7 @@ internal abstract class IconDecoderCore : ImageDecoderCore
metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata); metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata);
} }
return new(this.Dimensions, metadata, frames); return new ImageInfo(this.Dimensions, metadata, frames);
} }
protected abstract void SetFrameMetadata( protected abstract void SetFrameMetadata(
@ -276,20 +276,20 @@ internal abstract class IconDecoderCore : ImageDecoderCore
height = Math.Max(height, entry.Height); height = Math.Max(height, entry.Height);
} }
this.Dimensions = new(width, height); this.Dimensions = new Size(width, height);
} }
private ImageDecoderCore GetDecoder(bool isPng) private ImageDecoderCore GetDecoder(bool isPng)
{ {
if (isPng) if (isPng)
{ {
return new PngDecoderCore(new() return new PngDecoderCore(new PngDecoderOptions
{ {
GeneralOptions = this.Options, GeneralOptions = this.Options,
}); });
} }
return new BmpDecoderCore(new() return new BmpDecoderCore(new BmpDecoderOptions
{ {
GeneralOptions = this.Options, GeneralOptions = this.Options,
ProcessedAlphaMask = true, ProcessedAlphaMask = true,

6
src/ImageSharp/Formats/Icon/IconEncoderCore.cs

@ -116,7 +116,7 @@ internal abstract class IconEncoderCore
[MemberNotNull(nameof(entries))] [MemberNotNull(nameof(entries))]
private void InitHeader(Image image) private void InitHeader(Image image)
{ {
this.fileHeader = new(this.iconFileType, (ushort)image.Frames.Count); this.fileHeader = new IconDir(this.iconFileType, (ushort)image.Frames.Count);
this.entries = this.iconFileType switch this.entries = this.iconFileType switch
{ {
IconFileType.ICO => IconFileType.ICO =>
@ -155,14 +155,14 @@ internal abstract class IconEncoderCore
count = 256; count = 256;
} }
return new WuQuantizer(new() return new WuQuantizer(new QuantizerOptions
{ {
MaxColors = count MaxColors = count
}); });
} }
// Don't dither if we have a palette. We want to preserve as much information as possible. // Don't dither if we have a palette. We want to preserve as much information as possible.
return new PaletteQuantizer(metadata.ColorTable.Value, new() { Dither = null }); return new PaletteQuantizer(metadata.ColorTable.Value, new QuantizerOptions { Dither = null });
} }
internal sealed class EncodingFrameMetadata internal sealed class EncodingFrameMetadata

16
src/ImageSharp/Formats/ImageDecoder.cs

@ -328,6 +328,14 @@ public abstract class ImageDecoder : IImageDecoder
{ {
image.Metadata.IccProfile = null; image.Metadata.IccProfile = null;
} }
foreach (ImageFrame frame in image.Frames)
{
if (options.CanRemoveIccProfile(frame.Metadata.IccProfile))
{
frame.Metadata.IccProfile = null;
}
}
} }
private static void HandleIccProfile(DecoderOptions options, ImageInfo image) private static void HandleIccProfile(DecoderOptions options, ImageInfo image)
@ -336,5 +344,13 @@ public abstract class ImageDecoder : IImageDecoder
{ {
image.Metadata.IccProfile = null; image.Metadata.IccProfile = null;
} }
foreach (ImageFrameMetadata frame in image.FrameMetadataCollection)
{
if (options.CanRemoveIccProfile(frame.IccProfile))
{
frame.IccProfile = null;
}
}
} }
} }

93
src/ImageSharp/Formats/ImageDecoderCore.cs

@ -1,8 +1,12 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.ColorProfiles;
using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats; namespace SixLabors.ImageSharp.Formats;
@ -124,4 +128,93 @@ internal abstract class ImageDecoderCore
/// </remarks> /// </remarks>
protected abstract Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken) protected abstract Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>; where TPixel : unmanaged, IPixel<TPixel>;
/// <summary>
/// Converts the ICC color profile of the specified image to the compact sRGB v4 profile if a source profile is
/// available.
/// </summary>
/// <remarks>
/// This method should only be used by decoders that gurantee that the encoded image data is in a color space
/// compatible with sRGB (e.g. standard RGB, Adobe RGB, ProPhoto RGB).
/// <br/>
/// If the image does not have a valid ICC profile for color conversion, no changes are made.
/// This operation may affect the color appearance of the image to ensure consistency with the sRGB color
/// space.
/// </remarks>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The image whose ICC profile will be converted to the compact sRGB v4 profile.</param>
/// <returns>
/// <see langword="true"/> if the conversion was performed; otherwise, <see langword="false"/>.
/// </returns>
protected bool TryConvertIccProfile<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
if (!this.Options.TryGetIccProfileForColorConversion(image.Metadata.IccProfile, out IccProfile? profile))
{
return false;
}
ColorConversionOptions options = new()
{
SourceIccProfile = profile,
TargetIccProfile = CompactSrgbV4Profile.Profile,
MemoryAllocator = image.Configuration.MemoryAllocator,
};
ColorProfileConverter converter = new(options);
converter.Convert(image);
return true;
}
/// <summary>
/// Converts the ICC color profile of the specified image frame to the compact sRGB v4 profile if a source profile is
/// available.
/// </summary>
/// <remarks>
/// This method should only be used by decoders that gurantee that the encoded image data is in a color space
/// compatible with sRGB (e.g. standard RGB, Adobe RGB, ProPhoto RGB).
/// <br/>
/// If the image does not have a valid ICC profile for color conversion, no changes are made.
/// This operation may affect the color appearance of the image to ensure consistency with the sRGB color
/// space.
/// </remarks>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The image frame whose ICC profile will be converted to the compact sRGB v4 profile.</param>
/// <returns>
/// <see langword="true"/> if the conversion was performed; otherwise, <see langword="false"/>.
/// </returns>
protected bool TryConvertIccProfile<TPixel>(ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel>
{
if (!this.Options.TryGetIccProfileForColorConversion(frame.Metadata.IccProfile, out IccProfile? profile))
{
return false;
}
ColorConversionOptions options = new()
{
SourceIccProfile = profile,
TargetIccProfile = CompactSrgbV4Profile.Profile,
MemoryAllocator = frame.Configuration.MemoryAllocator,
};
ColorProfileConverter converter = new(options);
ImageMetadata metadata = new()
{
IccProfile = frame.Metadata.IccProfile
};
IMemoryGroup<TPixel> m = frame.PixelBuffer.MemoryGroup;
// Safe: ToArray only materializes the Memory<TPixel> segment list, not the underlying pixel buffers,
// and Wrap(Memory<T>[]) creates a Consumed MemoryGroup that does not own the buffers (Dispose just
// invalidates the view). This means no pixel data is cloned and disposing the temporary image will
// not dispose or leak the frame's pixel buffer.
MemoryGroup<TPixel> memorySource = MemoryGroup<TPixel>.Wrap(m.ToArray());
using Image<TPixel> image = new(frame.Configuration, memorySource, frame.Width, frame.Height, metadata);
converter.Convert(image);
return true;
}
} }

2
src/ImageSharp/Formats/ImageFormatManager.cs

@ -161,7 +161,7 @@ public class ImageFormatManager
/// <summary> /// <summary>
/// Removes all the registered image format detectors. /// Removes all the registered image format detectors.
/// </summary> /// </summary>
public void ClearImageFormatDetectors() => this.imageFormatDetectors = new(); public void ClearImageFormatDetectors() => this.imageFormatDetectors = new ConcurrentBag<IImageFormatDetector>();
/// <summary> /// <summary>
/// Adds a new detector for detecting mime types. /// Adds a new detector for detecting mime types.

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

@ -140,7 +140,7 @@ internal partial struct Block8x8
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() public override string ToString()
{ {
var sb = new StringBuilder(); StringBuilder sb = new();
sb.Append('['); sb.Append('[');
for (int i = 0; i < Size; i++) for (int i = 0; i < Size; i++)
{ {

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

@ -57,19 +57,19 @@ internal partial struct Block8x8F
ref Vector4 dTopLeft = ref Unsafe.As<Vector2, Vector4>(ref Unsafe.Add(ref destBase, offset)); ref Vector4 dTopLeft = ref Unsafe.As<Vector2, Vector4>(ref Unsafe.Add(ref destBase, offset));
ref Vector4 dBottomLeft = ref Unsafe.As<Vector2, Vector4>(ref Unsafe.Add(ref destBase, offset + destStride)); ref Vector4 dBottomLeft = ref Unsafe.As<Vector2, Vector4>(ref Unsafe.Add(ref destBase, offset + destStride));
var xyLeft = new Vector4(sLeft.X); Vector4 xyLeft = new(sLeft.X);
xyLeft.Z = sLeft.Y; xyLeft.Z = sLeft.Y;
xyLeft.W = sLeft.Y; xyLeft.W = sLeft.Y;
var zwLeft = new Vector4(sLeft.Z); Vector4 zwLeft = new(sLeft.Z);
zwLeft.Z = sLeft.W; zwLeft.Z = sLeft.W;
zwLeft.W = sLeft.W; zwLeft.W = sLeft.W;
var xyRight = new Vector4(sRight.X); Vector4 xyRight = new(sRight.X);
xyRight.Z = sRight.Y; xyRight.Z = sRight.Y;
xyRight.W = sRight.Y; xyRight.W = sRight.Y;
var zwRight = new Vector4(sRight.Z); Vector4 zwRight = new(sRight.Z);
zwRight.Z = sRight.W; zwRight.Z = sRight.W;
zwRight.W = sRight.W; zwRight.W = sRight.W;

4
src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.TiffYccKScalar.cs

@ -128,7 +128,7 @@ internal abstract partial class JpegColorConverterBase
ColorProfileConverter converter = new(); ColorProfileConverter converter = new();
Span<Cmyk> source = MemoryMarshal.Cast<float, Cmyk>(packed); Span<Cmyk> source = MemoryMarshal.Cast<float, Cmyk>(packed);
// YccK is not a defined ICC color space it's a JPEG-specific encoding used in Adobe-style CMYK JPEGs. // YccK is not a defined ICC color space it's a JPEG-specific encoding used in Adobe-style CMYK JPEGs.
// ICC profiles expect colorimetric CMYK values, so we must first convert YccK to CMYK using a hardcoded inverse transform. // ICC profiles expect colorimetric CMYK values, so we must first convert YccK to CMYK using a hardcoded inverse transform.
// This transform assumes Rec.601 YCbCr coefficients and an inverted K channel. // This transform assumes Rec.601 YCbCr coefficients and an inverted K channel.
// //
@ -144,7 +144,7 @@ internal abstract partial class JpegColorConverterBase
SourceIccProfile = profile, SourceIccProfile = profile,
TargetIccProfile = CompactSrgbV4Profile.Profile, TargetIccProfile = CompactSrgbV4Profile.Profile,
}; };
converter = new(options); converter = new ColorProfileConverter(options);
converter.Convert<Cmyk, Rgb>(source, destination); converter.Convert<Cmyk, Rgb>(source, destination);
UnpackDeinterleave3(MemoryMarshal.Cast<float, Vector3>(packed)[..source.Length], c0, c1, c2); UnpackDeinterleave3(MemoryMarshal.Cast<float, Vector3>(packed)[..source.Length], c0, c1, c2);

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

@ -91,7 +91,7 @@ internal abstract partial class JpegColorConverterBase
SourceIccProfile = profile, SourceIccProfile = profile,
TargetIccProfile = CompactSrgbV4Profile.Profile, TargetIccProfile = CompactSrgbV4Profile.Profile,
}; };
converter = new(options); converter = new ColorProfileConverter(options);
converter.Convert<Rgb, Rgb>(destination, destination); converter.Convert<Rgb, Rgb>(destination, destination);
UnpackDeinterleave3(MemoryMarshal.Cast<float, Vector3>(packed)[..source.Length], c0, c1, c2); UnpackDeinterleave3(MemoryMarshal.Cast<float, Vector3>(packed)[..source.Length], c0, c1, c2);

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

@ -116,7 +116,7 @@ internal abstract partial class JpegColorConverterBase
SourceIccProfile = profile, SourceIccProfile = profile,
TargetIccProfile = CompactSrgbV4Profile.Profile, TargetIccProfile = CompactSrgbV4Profile.Profile,
}; };
converter = new(options); converter = new ColorProfileConverter(options);
converter.Convert<Cmyk, Rgb>(source, destination); converter.Convert<Cmyk, Rgb>(source, destination);
UnpackDeinterleave3(MemoryMarshal.Cast<float, Vector3>(packed)[..source.Length], c0, c1, c2); UnpackDeinterleave3(MemoryMarshal.Cast<float, Vector3>(packed)[..source.Length], c0, c1, c2);

54
src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs

@ -11,80 +11,80 @@ internal static class ProfileResolver
/// <summary> /// <summary>
/// Gets the JFIF specific markers. /// Gets the JFIF specific markers.
/// </summary> /// </summary>
public static ReadOnlySpan<byte> JFifMarker => new[] public static ReadOnlySpan<byte> JFifMarker =>
{ [
(byte)'J', (byte)'F', (byte)'I', (byte)'F', (byte)'\0' (byte)'J', (byte)'F', (byte)'I', (byte)'F', (byte)'\0'
}; ];
/// <summary> /// <summary>
/// Gets the JFXX specific markers. /// Gets the JFXX specific markers.
/// </summary> /// </summary>
public static ReadOnlySpan<byte> JFxxMarker => new[] public static ReadOnlySpan<byte> JFxxMarker =>
{ [
(byte)'J', (byte)'F', (byte)'X', (byte)'X', (byte)'\0' (byte)'J', (byte)'F', (byte)'X', (byte)'X', (byte)'\0'
}; ];
/// <summary> /// <summary>
/// Gets the ICC specific markers. /// Gets the ICC specific markers.
/// </summary> /// </summary>
public static ReadOnlySpan<byte> IccMarker => new[] public static ReadOnlySpan<byte> IccMarker =>
{ [
(byte)'I', (byte)'C', (byte)'C', (byte)'_', (byte)'I', (byte)'C', (byte)'C', (byte)'_',
(byte)'P', (byte)'R', (byte)'O', (byte)'F', (byte)'P', (byte)'R', (byte)'O', (byte)'F',
(byte)'I', (byte)'L', (byte)'E', (byte)'\0' (byte)'I', (byte)'L', (byte)'E', (byte)'\0'
}; ];
/// <summary> /// <summary>
/// Gets the adobe photoshop APP13 marker which can contain IPTC meta data. /// Gets the adobe photoshop APP13 marker which can contain IPTC meta data.
/// </summary> /// </summary>
public static ReadOnlySpan<byte> AdobePhotoshopApp13Marker => new[] public static ReadOnlySpan<byte> AdobePhotoshopApp13Marker =>
{ [
(byte)'P', (byte)'h', (byte)'o', (byte)'t', (byte)'o', (byte)'s', (byte)'h', (byte)'o', (byte)'p', (byte)' ', (byte)'3', (byte)'.', (byte)'0', (byte)'\0' (byte)'P', (byte)'h', (byte)'o', (byte)'t', (byte)'o', (byte)'s', (byte)'h', (byte)'o', (byte)'p', (byte)' ', (byte)'3', (byte)'.', (byte)'0', (byte)'\0'
}; ];
/// <summary> /// <summary>
/// Gets the 8BIM marker, which signals the start of a adobe specific image resource block. /// Gets the 8BIM marker, which signals the start of a adobe specific image resource block.
/// </summary> /// </summary>
public static ReadOnlySpan<byte> AdobeImageResourceBlockMarker => new[] public static ReadOnlySpan<byte> AdobeImageResourceBlockMarker =>
{ [
(byte)'8', (byte)'B', (byte)'I', (byte)'M' (byte)'8', (byte)'B', (byte)'I', (byte)'M'
}; ];
/// <summary> /// <summary>
/// Gets a IPTC Image resource ID. /// Gets a IPTC Image resource ID.
/// </summary> /// </summary>
public static ReadOnlySpan<byte> AdobeIptcMarker => new[] public static ReadOnlySpan<byte> AdobeIptcMarker =>
{ [
(byte)4, (byte)4 (byte)4, (byte)4
}; ];
/// <summary> /// <summary>
/// Gets the EXIF specific markers. /// Gets the EXIF specific markers.
/// </summary> /// </summary>
public static ReadOnlySpan<byte> ExifMarker => new[] public static ReadOnlySpan<byte> ExifMarker =>
{ [
(byte)'E', (byte)'x', (byte)'i', (byte)'f', (byte)'\0', (byte)'\0' (byte)'E', (byte)'x', (byte)'i', (byte)'f', (byte)'\0', (byte)'\0'
}; ];
/// <summary> /// <summary>
/// Gets the XMP specific markers. /// Gets the XMP specific markers.
/// </summary> /// </summary>
public static ReadOnlySpan<byte> XmpMarker => new[] public static ReadOnlySpan<byte> XmpMarker =>
{ [
(byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/',
(byte)'n', (byte)'s', (byte)'.', (byte)'a', (byte)'d', (byte)'o', (byte)'b', (byte)'n', (byte)'s', (byte)'.', (byte)'a', (byte)'d', (byte)'o', (byte)'b',
(byte)'e', (byte)'.', (byte)'c', (byte)'o', (byte)'m', (byte)'/', (byte)'x', (byte)'e', (byte)'.', (byte)'c', (byte)'o', (byte)'m', (byte)'/', (byte)'x',
(byte)'a', (byte)'p', (byte)'/', (byte)'1', (byte)'.', (byte)'0', (byte)'/', (byte)'a', (byte)'p', (byte)'/', (byte)'1', (byte)'.', (byte)'0', (byte)'/',
(byte)0 (byte)0
}; ];
/// <summary> /// <summary>
/// Gets the Adobe specific markers <see href="http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html#Adobe"/>. /// Gets the Adobe specific markers <see href="http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html#Adobe"/>.
/// </summary> /// </summary>
public static ReadOnlySpan<byte> AdobeMarker => new[] public static ReadOnlySpan<byte> AdobeMarker =>
{ [
(byte)'A', (byte)'d', (byte)'o', (byte)'b', (byte)'e' (byte)'A', (byte)'d', (byte)'o', (byte)'b', (byte)'e'
}; ];
/// <summary> /// <summary>
/// Returns a value indicating whether the passed bytes are a match to the profile identifier. /// Returns a value indicating whether the passed bytes are a match to the profile identifier.

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

@ -241,7 +241,7 @@ internal class ComponentProcessor : IDisposable
ref Vector<float> targetVectorRef = ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(target)); ref Vector<float> targetVectorRef = ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(target));
nuint count = target.VectorCount<float>(); nuint count = target.VectorCount<float>();
var multiplierVector = new Vector<float>(multiplier); Vector<float> multiplierVector = new(multiplier);
for (nuint i = 0; i < count; i++) for (nuint i = 0; i < count; i++)
{ {
Unsafe.Add(ref targetVectorRef, i) *= multiplierVector; Unsafe.Add(ref targetVectorRef, i) *= multiplierVector;

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

@ -409,7 +409,7 @@ internal class HuffmanScanEncoder
{ {
this.FlushRemainingBytes(); this.FlushRemainingBytes();
this.WriteRestart(restarts % 8); this.WriteRestart(restarts % 8);
foreach (var component in frame.Components) foreach (Component component in frame.Components)
{ {
component.DcPredictor = 0; component.DcPredictor = 0;
} }

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

@ -15,15 +15,13 @@ internal readonly struct HuffmanSpec
/// This is an example specification taken from the jpeg specification paper. /// This is an example specification taken from the jpeg specification paper.
/// </remarks> /// </remarks>
public static readonly HuffmanSpec LuminanceDC = new( public static readonly HuffmanSpec LuminanceDC = new(
new byte[] [
{
0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0 0, 0, 0
}, ],
new byte[] [
{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
}); ]);
/// <summary> /// <summary>
/// Huffman talbe specification for luminance AC. /// Huffman talbe specification for luminance AC.
@ -32,13 +30,11 @@ internal readonly struct HuffmanSpec
/// This is an example specification taken from the jpeg specification paper. /// This is an example specification taken from the jpeg specification paper.
/// </remarks> /// </remarks>
public static readonly HuffmanSpec LuminanceAC = new( public static readonly HuffmanSpec LuminanceAC = new(
new byte[] [
{
0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0,
0, 1, 125 0, 1, 125
}, ],
new byte[] [
{
0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11,
0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13,
0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32,
@ -63,7 +59,7 @@ internal readonly struct HuffmanSpec
0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1,
0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
0xf9, 0xfa 0xf9, 0xfa
}); ]);
/// <summary> /// <summary>
/// Huffman talbe specification for chrominance DC. /// Huffman talbe specification for chrominance DC.
@ -72,15 +68,13 @@ internal readonly struct HuffmanSpec
/// This is an example specification taken from the jpeg specification paper. /// This is an example specification taken from the jpeg specification paper.
/// </remarks> /// </remarks>
public static readonly HuffmanSpec ChrominanceDC = new( public static readonly HuffmanSpec ChrominanceDC = new(
new byte[] [
{
0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 0 0, 0, 0
}, ],
new byte[] [
{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
}); ]);
/// <summary> /// <summary>
/// Huffman talbe specification for chrominance DC. /// Huffman talbe specification for chrominance DC.
@ -89,13 +83,11 @@ internal readonly struct HuffmanSpec
/// This is an example specification taken from the jpeg specification paper. /// This is an example specification taken from the jpeg specification paper.
/// </remarks> /// </remarks>
public static readonly HuffmanSpec ChrominanceAC = new( public static readonly HuffmanSpec ChrominanceAC = new(
new byte[] [
{
0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0,
1, 2, 119 1, 2, 119
}, ],
new byte[] [
{
0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04,
0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51,
0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81,
@ -120,7 +112,7 @@ internal readonly struct HuffmanSpec
0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,
0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
0xf9, 0xfa 0xf9, 0xfa
}); ]);
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="HuffmanSpec"/> struct. /// Initializes a new instance of the <see cref="HuffmanSpec"/> struct.

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

@ -46,7 +46,7 @@ internal class SpectralConverter<TPixel> : SpectralConverter, IDisposable
// component processors from spectral to Rgb24 // component processors from spectral to Rgb24
const int blockPixelWidth = 8; const int blockPixelWidth = 8;
this.alignedPixelWidth = majorBlockWidth * blockPixelWidth; this.alignedPixelWidth = majorBlockWidth * blockPixelWidth;
var postProcessorBufferSize = new Size(this.alignedPixelWidth, this.pixelRowsPerStep); Size postProcessorBufferSize = new(this.alignedPixelWidth, this.pixelRowsPerStep);
this.componentProcessors = new ComponentProcessor[frame.Components.Length]; this.componentProcessors = new ComponentProcessor[frame.Components.Length];
for (int i = 0; i < this.componentProcessors.Length; i++) for (int i = 0; i < this.componentProcessors.Length; i++)
{ {
@ -119,7 +119,7 @@ internal class SpectralConverter<TPixel> : SpectralConverter, IDisposable
bLane.Slice(paddingStartIndex).Fill(bLane[paddingStartIndex - 1]); bLane.Slice(paddingStartIndex).Fill(bLane[paddingStartIndex - 1]);
// Convert from rgb24 to target pixel type // Convert from rgb24 to target pixel type
var values = new JpegColorConverterBase.ComponentValues(this.componentProcessors, y); JpegColorConverterBase.ComponentValues values = new(this.componentProcessors, y);
this.colorConverter.ConvertFromRgb(values, rLane, gLane, bLane); this.colorConverter.ConvertFromRgb(values, rLane, gLane, bLane);
} }

8
src/ImageSharp/Formats/Jpeg/Components/FloatingPointDCT.cs

@ -49,8 +49,8 @@ internal static partial class FloatingPointDCT
/// </code> /// </code>
/// </para> /// </para>
/// </remarks> /// </remarks>
private static readonly float[] AdjustmentCoefficients = new float[] private static readonly float[] AdjustmentCoefficients =
{ [
1f, 1.3870399f, 1.306563f, 1.1758755f, 1f, 0.78569496f, 0.5411961f, 0.27589938f, 1f, 1.3870399f, 1.306563f, 1.1758755f, 1f, 0.78569496f, 0.5411961f, 0.27589938f,
1.3870399f, 1.9238797f, 1.812255f, 1.6309863f, 1.3870399f, 1.0897902f, 0.7506606f, 0.38268346f, 1.3870399f, 1.9238797f, 1.812255f, 1.6309863f, 1.3870399f, 1.0897902f, 0.7506606f, 0.38268346f,
1.306563f, 1.812255f, 1.707107f, 1.5363555f, 1.306563f, 1.02656f, 0.7071068f, 0.36047992f, 1.306563f, 1.812255f, 1.707107f, 1.5363555f, 1.306563f, 1.02656f, 0.7071068f, 0.36047992f,
@ -58,8 +58,8 @@ internal static partial class FloatingPointDCT
1f, 1.3870399f, 1.306563f, 1.1758755f, 1f, 0.78569496f, 0.5411961f, 0.27589938f, 1f, 1.3870399f, 1.306563f, 1.1758755f, 1f, 0.78569496f, 0.5411961f, 0.27589938f,
0.78569496f, 1.0897902f, 1.02656f, 0.9238795f, 0.78569496f, 0.61731654f, 0.42521507f, 0.21677275f, 0.78569496f, 1.0897902f, 1.02656f, 0.9238795f, 0.78569496f, 0.61731654f, 0.42521507f, 0.21677275f,
0.5411961f, 0.7506606f, 0.7071068f, 0.63637924f, 0.5411961f, 0.42521507f, 0.29289323f, 0.14931567f, 0.5411961f, 0.7506606f, 0.7071068f, 0.63637924f, 0.5411961f, 0.42521507f, 0.29289323f, 0.14931567f,
0.27589938f, 0.38268346f, 0.36047992f, 0.32442334f, 0.27589938f, 0.21677275f, 0.14931567f, 0.076120466f, 0.27589938f, 0.38268346f, 0.36047992f, 0.32442334f, 0.27589938f, 0.21677275f, 0.14931567f, 0.076120466f
}; ];
/// <summary> /// <summary>
/// Adjusts given quantization table for usage with <see cref="TransformIDCT"/>. /// Adjusts given quantization table for usage with <see cref="TransformIDCT"/>.

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

@ -46,8 +46,8 @@ internal static class Quantization
// The C# compiler emits this as a compile-time constant embedded in the PE file. // The C# compiler emits this as a compile-time constant embedded in the PE file.
// This is effectively compiled down to: return new ReadOnlySpan<byte>(&data, length) // This is effectively compiled down to: return new ReadOnlySpan<byte>(&data, length)
// More details can be found: https://github.com/dotnet/roslyn/pull/24621 // More details can be found: https://github.com/dotnet/roslyn/pull/24621
public static ReadOnlySpan<byte> LuminanceTable => new byte[] public static ReadOnlySpan<byte> LuminanceTable =>
{ [
16, 11, 10, 16, 24, 40, 51, 61, 16, 11, 10, 16, 24, 40, 51, 61,
12, 12, 14, 19, 26, 58, 60, 55, 12, 12, 14, 19, 26, 58, 60, 55,
14, 13, 16, 24, 40, 57, 69, 56, 14, 13, 16, 24, 40, 57, 69, 56,
@ -55,8 +55,8 @@ internal static class Quantization
18, 22, 37, 56, 68, 109, 103, 77, 18, 22, 37, 56, 68, 109, 103, 77,
24, 35, 55, 64, 81, 104, 113, 92, 24, 35, 55, 64, 81, 104, 113, 92,
49, 64, 78, 87, 103, 121, 120, 101, 49, 64, 78, 87, 103, 121, 120, 101,
72, 92, 95, 98, 112, 100, 103, 99, 72, 92, 95, 98, 112, 100, 103, 99
}; ];
/// <summary> /// <summary>
/// Gets unscaled chrominance quantization table. /// Gets unscaled chrominance quantization table.
@ -67,8 +67,8 @@ internal static class Quantization
// The C# compiler emits this as a compile-time constant embedded in the PE file. // The C# compiler emits this as a compile-time constant embedded in the PE file.
// This is effectively compiled down to: return new ReadOnlySpan<byte>(&data, length) // This is effectively compiled down to: return new ReadOnlySpan<byte>(&data, length)
// More details can be found: https://github.com/dotnet/roslyn/pull/24621 // More details can be found: https://github.com/dotnet/roslyn/pull/24621
public static ReadOnlySpan<byte> ChrominanceTable => new byte[] public static ReadOnlySpan<byte> ChrominanceTable =>
{ [
17, 18, 24, 47, 99, 99, 99, 99, 17, 18, 24, 47, 99, 99, 99, 99,
18, 21, 26, 66, 99, 99, 99, 99, 18, 21, 26, 66, 99, 99, 99, 99,
24, 26, 56, 99, 99, 99, 99, 99, 24, 26, 56, 99, 99, 99, 99, 99,
@ -76,8 +76,8 @@ internal static class Quantization
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99
}; ];
/// Ported from JPEGsnoop: /// Ported from JPEGsnoop:
/// https://github.com/ImpulseAdventure/JPEGsnoop/blob/9732ee0961f100eb69bbff4a0c47438d5997abee/source/JfifDecode.cpp#L4570-L4694 /// https://github.com/ImpulseAdventure/JPEGsnoop/blob/9732ee0961f100eb69bbff4a0c47438d5997abee/source/JfifDecode.cpp#L4570-L4694

6
src/ImageSharp/Formats/Jpeg/Components/SizeExtensions.cs

@ -14,20 +14,20 @@ internal static class SizeExtensions
/// Multiplies 'a.Width' with 'b.Width' and 'a.Height' with 'b.Height'. /// Multiplies 'a.Width' with 'b.Width' and 'a.Height' with 'b.Height'.
/// TODO: Shouldn't we expose this as operator in SixLabors.Core? /// TODO: Shouldn't we expose this as operator in SixLabors.Core?
/// </summary> /// </summary>
public static Size MultiplyBy(this Size a, Size b) => new Size(a.Width * b.Width, a.Height * b.Height); public static Size MultiplyBy(this Size a, Size b) => new(a.Width * b.Width, a.Height * b.Height);
/// <summary> /// <summary>
/// Divides 'a.Width' with 'b.Width' and 'a.Height' with 'b.Height'. /// Divides 'a.Width' with 'b.Width' and 'a.Height' with 'b.Height'.
/// TODO: Shouldn't we expose this as operator in SixLabors.Core? /// TODO: Shouldn't we expose this as operator in SixLabors.Core?
/// </summary> /// </summary>
public static Size DivideBy(this Size a, Size b) => new Size(a.Width / b.Width, a.Height / b.Height); public static Size DivideBy(this Size a, Size b) => new(a.Width / b.Width, a.Height / b.Height);
/// <summary> /// <summary>
/// Divide Width and Height as real numbers and return the Ceiling. /// Divide Width and Height as real numbers and return the Ceiling.
/// </summary> /// </summary>
public static Size DivideRoundUp(this Size originalSize, int divX, int divY) public static Size DivideRoundUp(this Size originalSize, int divX, int divY)
{ {
var sizeVect = (Vector2)(SizeF)originalSize; Vector2 sizeVect = (Vector2)(SizeF)originalSize;
sizeVect /= new Vector2(divX, divY); sizeVect /= new Vector2(divX, divY);
sizeVect.X = MathF.Ceiling(sizeVect.X); sizeVect.X = MathF.Ceiling(sizeVect.X);
sizeVect.Y = MathF.Ceiling(sizeVect.Y); sizeVect.Y = MathF.Ceiling(sizeVect.Y);

12
src/ImageSharp/Formats/Jpeg/Components/ZigZag.cs

@ -18,8 +18,8 @@ internal static partial class ZigZag
/// The worst case would be a run-length of 15, which means we need 16 /// The worst case would be a run-length of 15, which means we need 16
/// fake entries. /// fake entries.
/// </remarks> /// </remarks>
public static ReadOnlySpan<byte> ZigZagOrder => new byte[] public static ReadOnlySpan<byte> ZigZagOrder =>
{ [
0, 1, 8, 16, 9, 2, 3, 10, 0, 1, 8, 16, 9, 2, 3, 10,
17, 24, 32, 25, 18, 11, 4, 5, 17, 24, 32, 25, 18, 11, 4, 5,
12, 19, 26, 33, 40, 48, 41, 34, 12, 19, 26, 33, 40, 48, 41, 34,
@ -32,7 +32,7 @@ internal static partial class ZigZag
// Extra entries for safety in decoder // Extra entries for safety in decoder
63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63,
63, 63, 63, 63, 63, 63, 63, 63 63, 63, 63, 63, 63, 63, 63, 63
}; ];
/// <summary> /// <summary>
/// Gets span of zig-zag with fused transpose step ordering indices. /// Gets span of zig-zag with fused transpose step ordering indices.
@ -47,8 +47,8 @@ internal static partial class ZigZag
/// The worst case would be a run-length of 15, which means we need 16 /// The worst case would be a run-length of 15, which means we need 16
/// fake entries. /// fake entries.
/// </remarks> /// </remarks>
public static ReadOnlySpan<byte> TransposingOrder => new byte[] public static ReadOnlySpan<byte> TransposingOrder =>
{ [
0, 8, 1, 2, 9, 16, 24, 17, 0, 8, 1, 2, 9, 16, 24, 17,
10, 3, 4, 11, 18, 25, 32, 40, 10, 3, 4, 11, 18, 25, 32, 40,
33, 26, 19, 12, 5, 6, 13, 20, 33, 26, 19, 12, 5, 6, 13, 20,
@ -61,5 +61,5 @@ internal static partial class ZigZag
// Extra entries for safety in decoder // Extra entries for safety in decoder
63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63,
63, 63, 63, 63, 63, 63, 63, 63 63, 63, 63, 63, 63, 63, 63, 63
}; ];
} }

4
src/ImageSharp/Formats/Jpeg/JpegConstants.cs

@ -18,12 +18,12 @@ internal static class JpegConstants
/// <summary> /// <summary>
/// The list of mimetypes that equate to a jpeg. /// The list of mimetypes that equate to a jpeg.
/// </summary> /// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] { "image/jpeg", "image/pjpeg" }; public static readonly IEnumerable<string> MimeTypes = ["image/jpeg", "image/pjpeg"];
/// <summary> /// <summary>
/// The list of file extensions that equate to a jpeg. /// The list of file extensions that equate to a jpeg.
/// </summary> /// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "jpg", "jpeg", "jfif" }; public static readonly IEnumerable<string> FileExtensions = ["jpg", "jpeg", "jfif"];
/// <summary> /// <summary>
/// Contains marker specific constants. /// Contains marker specific constants.

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

@ -25,7 +25,7 @@ public sealed class JpegDecoder : SpecializedImageDecoder<JpegDecoderOptions>
Guard.NotNull(options, nameof(options)); Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
using JpegDecoderCore decoder = new(new() { GeneralOptions = options }); using JpegDecoderCore decoder = new(new JpegDecoderOptions { GeneralOptions = options });
return decoder.Identify(options.Configuration, stream, cancellationToken); return decoder.Identify(options.Configuration, stream, cancellationToken);
} }

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

@ -71,6 +71,11 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
/// </summary> /// </summary>
private bool hasAdobeMarker; private bool hasAdobeMarker;
/// <summary>
/// Whether the image has a SOS marker.
/// </summary>
private bool hasSOSMarker;
/// <summary> /// <summary>
/// Contains information about the JFIF marker. /// Contains information about the JFIF marker.
/// </summary> /// </summary>
@ -197,6 +202,12 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
{ {
using SpectralConverter<TPixel> spectralConverter = new(this.configuration, this.resizeMode == JpegDecoderResizeMode.ScaleOnly ? null : this.Options.TargetSize); using SpectralConverter<TPixel> spectralConverter = new(this.configuration, this.resizeMode == JpegDecoderResizeMode.ScaleOnly ? null : this.Options.TargetSize);
this.ParseStream(stream, spectralConverter, cancellationToken); this.ParseStream(stream, spectralConverter, cancellationToken);
if (!this.hasSOSMarker)
{
JpegThrowHelper.ThrowInvalidImageContentException("Missing SOS marker.");
}
this.InitExifProfile(); this.InitExifProfile();
this.InitIccProfile(); this.InitIccProfile();
this.InitIptcProfile(); this.InitIptcProfile();
@ -215,6 +226,12 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{ {
this.ParseStream(stream, spectralConverter: null, cancellationToken); this.ParseStream(stream, spectralConverter: null, cancellationToken);
if (!this.hasSOSMarker)
{
JpegThrowHelper.ThrowInvalidImageContentException("Missing SOS marker.");
}
this.InitExifProfile(); this.InitExifProfile();
this.InitIccProfile(); this.InitIccProfile();
this.InitIptcProfile(); this.InitIptcProfile();
@ -222,7 +239,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
this.InitDerivedMetadataProperties(); this.InitDerivedMetadataProperties();
Size pixelSize = this.Frame.PixelSize; Size pixelSize = this.Frame.PixelSize;
return new ImageInfo(new(pixelSize.Width, pixelSize.Height), this.Metadata); return new ImageInfo(new Size(pixelSize.Width, pixelSize.Height), this.Metadata);
} }
/// <summary> /// <summary>
@ -403,6 +420,8 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
break; break;
case JpegConstants.Markers.SOS: case JpegConstants.Markers.SOS:
this.hasSOSMarker = true;
if (!metadataOnly) if (!metadataOnly)
{ {
this.ProcessStartOfScanMarker(stream, markerContentByteSize); this.ProcessStartOfScanMarker(stream, markerContentByteSize);
@ -1243,7 +1262,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
} }
this.Frame = new JpegFrame(frameMarker, precision, frameWidth, frameHeight, componentCount); this.Frame = new JpegFrame(frameMarker, precision, frameWidth, frameHeight, componentCount);
this.Dimensions = new(frameWidth, frameHeight); this.Dimensions = new Size(frameWidth, frameHeight);
this.Metadata.GetJpegMetadata().Progressive = this.Frame.Progressive; this.Metadata.GetJpegMetadata().Progressive = this.Frame.Progressive;
remaining -= length; remaining -= length;

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

@ -13,15 +13,15 @@ internal sealed unsafe partial class JpegEncoderCore
{ {
private static JpegFrameConfig[] CreateFrameConfigs() private static JpegFrameConfig[] CreateFrameConfigs()
{ {
var defaultLuminanceHuffmanDC = new JpegHuffmanTableConfig(@class: 0, destIndex: 0, HuffmanSpec.LuminanceDC); JpegHuffmanTableConfig defaultLuminanceHuffmanDC = new(@class: 0, destIndex: 0, HuffmanSpec.LuminanceDC);
var defaultLuminanceHuffmanAC = new JpegHuffmanTableConfig(@class: 1, destIndex: 0, HuffmanSpec.LuminanceAC); JpegHuffmanTableConfig defaultLuminanceHuffmanAC = new(@class: 1, destIndex: 0, HuffmanSpec.LuminanceAC);
var defaultChrominanceHuffmanDC = new JpegHuffmanTableConfig(@class: 0, destIndex: 1, HuffmanSpec.ChrominanceDC); JpegHuffmanTableConfig defaultChrominanceHuffmanDC = new(@class: 0, destIndex: 1, HuffmanSpec.ChrominanceDC);
var defaultChrominanceHuffmanAC = new JpegHuffmanTableConfig(@class: 1, destIndex: 1, HuffmanSpec.ChrominanceAC); JpegHuffmanTableConfig defaultChrominanceHuffmanAC = new(@class: 1, destIndex: 1, HuffmanSpec.ChrominanceAC);
var defaultLuminanceQuantTable = new JpegQuantizationTableConfig(0, Quantization.LuminanceTable); JpegQuantizationTableConfig defaultLuminanceQuantTable = new(0, Quantization.LuminanceTable);
var defaultChrominanceQuantTable = new JpegQuantizationTableConfig(1, Quantization.ChrominanceTable); JpegQuantizationTableConfig defaultChrominanceQuantTable = new(1, Quantization.ChrominanceTable);
var yCbCrHuffmanConfigs = new JpegHuffmanTableConfig[] JpegHuffmanTableConfig[] yCbCrHuffmanConfigs = new JpegHuffmanTableConfig[]
{ {
defaultLuminanceHuffmanDC, defaultLuminanceHuffmanDC,
defaultLuminanceHuffmanAC, defaultLuminanceHuffmanAC,
@ -29,7 +29,7 @@ internal sealed unsafe partial class JpegEncoderCore
defaultChrominanceHuffmanAC, defaultChrominanceHuffmanAC,
}; };
var yCbCrQuantTableConfigs = new JpegQuantizationTableConfig[] JpegQuantizationTableConfig[] yCbCrQuantTableConfigs = new JpegQuantizationTableConfig[]
{ {
defaultLuminanceQuantTable, defaultLuminanceQuantTable,
defaultChrominanceQuantTable, defaultChrominanceQuantTable,
@ -38,77 +38,77 @@ internal sealed unsafe partial class JpegEncoderCore
return new JpegFrameConfig[] return new JpegFrameConfig[]
{ {
// YCbCr 4:4:4 // YCbCr 4:4:4
new JpegFrameConfig( new(
JpegColorSpace.YCbCr, JpegColorSpace.YCbCr,
JpegColorType.YCbCrRatio444, JpegColorType.YCbCrRatio444,
new JpegComponentConfig[] new JpegComponentConfig[]
{ {
new JpegComponentConfig(id: 1, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new(id: 1, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 2, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1), new(id: 2, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1),
new JpegComponentConfig(id: 3, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1), new(id: 3, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1),
}, },
yCbCrHuffmanConfigs, yCbCrHuffmanConfigs,
yCbCrQuantTableConfigs), yCbCrQuantTableConfigs),
// YCbCr 4:2:2 // YCbCr 4:2:2
new JpegFrameConfig( new(
JpegColorSpace.YCbCr, JpegColorSpace.YCbCr,
JpegColorType.YCbCrRatio422, JpegColorType.YCbCrRatio422,
new JpegComponentConfig[] new JpegComponentConfig[]
{ {
new JpegComponentConfig(id: 1, hsf: 2, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new(id: 1, hsf: 2, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 2, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1), new(id: 2, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1),
new JpegComponentConfig(id: 3, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1), new(id: 3, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1),
}, },
yCbCrHuffmanConfigs, yCbCrHuffmanConfigs,
yCbCrQuantTableConfigs), yCbCrQuantTableConfigs),
// YCbCr 4:2:0 // YCbCr 4:2:0
new JpegFrameConfig( new(
JpegColorSpace.YCbCr, JpegColorSpace.YCbCr,
JpegColorType.YCbCrRatio420, JpegColorType.YCbCrRatio420,
new JpegComponentConfig[] new JpegComponentConfig[]
{ {
new JpegComponentConfig(id: 1, hsf: 2, vsf: 2, quantIndex: 0, dcIndex: 0, acIndex: 0), new(id: 1, hsf: 2, vsf: 2, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 2, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1), new(id: 2, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1),
new JpegComponentConfig(id: 3, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1), new(id: 3, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1),
}, },
yCbCrHuffmanConfigs, yCbCrHuffmanConfigs,
yCbCrQuantTableConfigs), yCbCrQuantTableConfigs),
// YCbCr 4:1:1 // YCbCr 4:1:1
new JpegFrameConfig( new(
JpegColorSpace.YCbCr, JpegColorSpace.YCbCr,
JpegColorType.YCbCrRatio411, JpegColorType.YCbCrRatio411,
new JpegComponentConfig[] new JpegComponentConfig[]
{ {
new JpegComponentConfig(id: 1, hsf: 4, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new(id: 1, hsf: 4, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 2, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1), new(id: 2, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1),
new JpegComponentConfig(id: 3, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1), new(id: 3, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1),
}, },
yCbCrHuffmanConfigs, yCbCrHuffmanConfigs,
yCbCrQuantTableConfigs), yCbCrQuantTableConfigs),
// YCbCr 4:1:0 // YCbCr 4:1:0
new JpegFrameConfig( new(
JpegColorSpace.YCbCr, JpegColorSpace.YCbCr,
JpegColorType.YCbCrRatio410, JpegColorType.YCbCrRatio410,
new JpegComponentConfig[] new JpegComponentConfig[]
{ {
new JpegComponentConfig(id: 1, hsf: 4, vsf: 2, quantIndex: 0, dcIndex: 0, acIndex: 0), new(id: 1, hsf: 4, vsf: 2, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 2, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1), new(id: 2, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1),
new JpegComponentConfig(id: 3, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1), new(id: 3, hsf: 1, vsf: 1, quantIndex: 1, dcIndex: 1, acIndex: 1),
}, },
yCbCrHuffmanConfigs, yCbCrHuffmanConfigs,
yCbCrQuantTableConfigs), yCbCrQuantTableConfigs),
// Luminance // Luminance
new JpegFrameConfig( new(
JpegColorSpace.Grayscale, JpegColorSpace.Grayscale,
JpegColorType.Luminance, JpegColorType.Luminance,
new JpegComponentConfig[] new JpegComponentConfig[]
{ {
new JpegComponentConfig(id: 0, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new(id: 0, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
}, },
new JpegHuffmanTableConfig[] new JpegHuffmanTableConfig[]
{ {
@ -121,14 +121,14 @@ internal sealed unsafe partial class JpegEncoderCore
}), }),
// Rgb // Rgb
new JpegFrameConfig( new(
JpegColorSpace.RGB, JpegColorSpace.RGB,
JpegColorType.Rgb, JpegColorType.Rgb,
new JpegComponentConfig[] new JpegComponentConfig[]
{ {
new JpegComponentConfig(id: 82, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new(id: 82, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 71, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new(id: 71, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 66, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new(id: 66, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
}, },
new JpegHuffmanTableConfig[] new JpegHuffmanTableConfig[]
{ {
@ -144,15 +144,15 @@ internal sealed unsafe partial class JpegEncoderCore
}, },
// Cmyk // Cmyk
new JpegFrameConfig( new(
JpegColorSpace.Cmyk, JpegColorSpace.Cmyk,
JpegColorType.Cmyk, JpegColorType.Cmyk,
new JpegComponentConfig[] new JpegComponentConfig[]
{ {
new JpegComponentConfig(id: 1, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new(id: 1, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 2, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new(id: 2, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 3, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new(id: 3, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 4, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new(id: 4, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
}, },
new JpegHuffmanTableConfig[] new JpegHuffmanTableConfig[]
{ {
@ -168,15 +168,15 @@ internal sealed unsafe partial class JpegEncoderCore
}, },
// YccK // YccK
new JpegFrameConfig( new(
JpegColorSpace.Ycck, JpegColorSpace.Ycck,
JpegColorType.Ycck, JpegColorType.Ycck,
new JpegComponentConfig[] new JpegComponentConfig[]
{ {
new JpegComponentConfig(id: 1, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new(id: 1, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 2, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new(id: 2, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 3, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new(id: 3, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
new JpegComponentConfig(id: 4, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0), new(id: 4, hsf: 1, vsf: 1, quantIndex: 0, dcIndex: 0, acIndex: 0),
}, },
new JpegHuffmanTableConfig[] new JpegHuffmanTableConfig[]
{ {

2
src/ImageSharp/Formats/Jpeg/JpegFormat.cs

@ -15,7 +15,7 @@ public sealed class JpegFormat : IImageFormat<JpegMetadata>
/// <summary> /// <summary>
/// Gets the shared instance. /// Gets the shared instance.
/// </summary> /// </summary>
public static JpegFormat Instance { get; } = new JpegFormat(); public static JpegFormat Instance { get; } = new();
/// <inheritdoc/> /// <inheritdoc/>
public string Name => "JPEG"; public string Name => "JPEG";

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

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -200,7 +201,7 @@ public class JpegMetadata : IFormatMetadata<JpegMetadata>
}; };
/// <inheritdoc/> /// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination) public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
} }

4
src/ImageSharp/Formats/Pbm/PbmConstants.cs

@ -16,10 +16,10 @@ internal static class PbmConstants
/// <summary> /// <summary>
/// The list of mimetypes that equate to a ppm. /// The list of mimetypes that equate to a ppm.
/// </summary> /// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] { "image/x-portable-pixmap", "image/x-portable-anymap" }; public static readonly IEnumerable<string> MimeTypes = ["image/x-portable-pixmap", "image/x-portable-anymap"];
/// <summary> /// <summary>
/// The list of file extensions that equate to a ppm. /// The list of file extensions that equate to a ppm.
/// </summary> /// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "ppm", "pbm", "pgm" }; public static readonly IEnumerable<string> FileExtensions = ["ppm", "pbm", "pgm"];
} }

2
src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs

@ -80,7 +80,7 @@ internal sealed class PbmDecoderCore : ImageDecoderCore
{ {
this.ProcessHeader(stream); this.ProcessHeader(stream);
return new ImageInfo( return new ImageInfo(
new(this.pixelSize.Width, this.pixelSize.Height), new Size(this.pixelSize.Width, this.pixelSize.Height),
this.metadata); this.metadata);
} }

3
src/ImageSharp/Formats/Pbm/PbmMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Pbm; namespace SixLabors.ImageSharp.Formats.Pbm;
@ -130,7 +131,7 @@ public class PbmMetadata : IFormatMetadata<PbmMetadata>
}; };
/// <inheritdoc/> /// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination) public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
} }

8
src/ImageSharp/Formats/Png/Adam7.cs

@ -13,22 +13,22 @@ internal static class Adam7
/// <summary> /// <summary>
/// The amount to increment when processing each column per scanline for each interlaced pass. /// The amount to increment when processing each column per scanline for each interlaced pass.
/// </summary> /// </summary>
public static readonly int[] ColumnIncrement = { 8, 8, 4, 4, 2, 2, 1 }; public static readonly int[] ColumnIncrement = [8, 8, 4, 4, 2, 2, 1];
/// <summary> /// <summary>
/// The index to start at when processing each column per scanline for each interlaced pass. /// The index to start at when processing each column per scanline for each interlaced pass.
/// </summary> /// </summary>
public static readonly int[] FirstColumn = { 0, 4, 0, 2, 0, 1, 0 }; public static readonly int[] FirstColumn = [0, 4, 0, 2, 0, 1, 0];
/// <summary> /// <summary>
/// The index to start at when processing each row per scanline for each interlaced pass. /// The index to start at when processing each row per scanline for each interlaced pass.
/// </summary> /// </summary>
public static readonly int[] FirstRow = { 0, 0, 4, 0, 2, 0, 1 }; public static readonly int[] FirstRow = [0, 0, 4, 0, 2, 0, 1];
/// <summary> /// <summary>
/// The amount to increment when processing each row per scanline for each interlaced pass. /// The amount to increment when processing each row per scanline for each interlaced pass.
/// </summary> /// </summary>
public static readonly int[] RowIncrement = { 8, 8, 8, 4, 4, 2, 2 }; public static readonly int[] RowIncrement = [8, 8, 8, 4, 4, 2, 2];
/// <summary> /// <summary>
/// Gets the width of the block. /// Gets the width of the block.

68
src/ImageSharp/Formats/Png/PngConstants.cs

@ -28,12 +28,12 @@ internal static class PngConstants
/// <summary> /// <summary>
/// The list of mimetypes that equate to a Png. /// The list of mimetypes that equate to a Png.
/// </summary> /// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] { "image/png", "image/apng" }; public static readonly IEnumerable<string> MimeTypes = ["image/png", "image/apng"];
/// <summary> /// <summary>
/// The list of file extensions that equate to a Png. /// The list of file extensions that equate to a Png.
/// </summary> /// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "png", "apng" }; public static readonly IEnumerable<string> FileExtensions = ["png", "apng"];
/// <summary> /// <summary>
/// The header bytes as a big-endian coded ulong. /// The header bytes as a big-endian coded ulong.
@ -45,11 +45,11 @@ internal static class PngConstants
/// </summary> /// </summary>
public static readonly Dictionary<PngColorType, byte[]> ColorTypes = new() public static readonly Dictionary<PngColorType, byte[]> ColorTypes = new()
{ {
[PngColorType.Grayscale] = new byte[] { 1, 2, 4, 8, 16 }, [PngColorType.Grayscale] = [1, 2, 4, 8, 16],
[PngColorType.Rgb] = new byte[] { 8, 16 }, [PngColorType.Rgb] = [8, 16],
[PngColorType.Palette] = new byte[] { 1, 2, 4, 8 }, [PngColorType.Palette] = [1, 2, 4, 8],
[PngColorType.GrayscaleWithAlpha] = new byte[] { 8, 16 }, [PngColorType.GrayscaleWithAlpha] = [8, 16],
[PngColorType.RgbWithAlpha] = new byte[] { 8, 16 } [PngColorType.RgbWithAlpha] = [8, 16]
}; };
/// <summary> /// <summary>
@ -62,11 +62,26 @@ internal static class PngConstants
/// </summary> /// </summary>
public const int MinTextKeywordLength = 1; public const int MinTextKeywordLength = 1;
/// <summary>
/// Specifies the keyword used to identify the Exif raw profile in image metadata.
/// </summary>
public const string ExifRawProfileKeyword = "Raw profile type exif";
/// <summary>
/// Specifies the profile keyword used to identify raw IPTC metadata within image files.
/// </summary>
public const string IptcRawProfileKeyword = "Raw profile type iptc";
/// <summary>
/// The IPTC resource id in Photoshop IRB. 0x0404 (big endian).
/// </summary>
public const ushort AdobeIptcResourceId = 0x0404;
/// <summary> /// <summary>
/// Gets the header bytes identifying a Png. /// Gets the header bytes identifying a Png.
/// </summary> /// </summary>
public static ReadOnlySpan<byte> HeaderBytes => new byte[] public static ReadOnlySpan<byte> HeaderBytes =>
{ [
0x89, // Set the high bit. 0x89, // Set the high bit.
0x50, // P 0x50, // P
0x4E, // N 0x4E, // N
@ -75,13 +90,13 @@ internal static class PngConstants
0x0A, // Line ending CRLF 0x0A, // Line ending CRLF
0x1A, // EOF 0x1A, // EOF
0x0A // LF 0x0A // LF
}; ];
/// <summary> /// <summary>
/// Gets the keyword of the XMP metadata, encoded in an iTXT chunk. /// Gets the keyword of the XMP metadata, encoded in an iTXT chunk.
/// </summary> /// </summary>
public static ReadOnlySpan<byte> XmpKeyword => new[] public static ReadOnlySpan<byte> XmpKeyword =>
{ [
(byte)'X', (byte)'X',
(byte)'M', (byte)'M',
(byte)'L', (byte)'L',
@ -99,5 +114,32 @@ internal static class PngConstants
(byte)'x', (byte)'x',
(byte)'m', (byte)'m',
(byte)'p' (byte)'p'
}; ];
/// <summary>
/// Gets the ASCII bytes for the "Photoshop 3.0" identifier used in some PNG metadata payloads.
/// This value is null-terminated.
/// </summary>
public static ReadOnlySpan<byte> AdobePhotoshop30 =>
[
(byte)'P',
(byte)'h',
(byte)'o',
(byte)'t',
(byte)'o',
(byte)'s',
(byte)'h',
(byte)'o',
(byte)'p',
(byte)' ',
(byte)'3',
(byte)'.',
(byte)'0',
0
];
/// <summary>
/// Gets the ASCII bytes for the "8BIM" signature used in Photoshop resources.
/// </summary>
public static ReadOnlySpan<byte> EightBim => [(byte)'8', (byte)'B', (byte)'I', (byte)'M'];
} }

2
src/ImageSharp/Formats/Png/PngDecoder.cs

@ -25,7 +25,7 @@ public sealed class PngDecoder : SpecializedImageDecoder<PngDecoderOptions>
Guard.NotNull(options, nameof(options)); Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
return new PngDecoderCore(new PngDecoderOptions() { GeneralOptions = options }).Identify(options.Configuration, stream, cancellationToken); return new PngDecoderCore(new PngDecoderOptions { GeneralOptions = options }).Identify(options.Configuration, stream, cancellationToken);
} }
/// <inheritdoc/> /// <inheritdoc/>

233
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -21,6 +21,7 @@ using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -212,6 +213,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
currentFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); currentFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan());
break; break;
case PngChunkType.FrameData: case PngChunkType.FrameData:
{
if (frameCount >= this.maxFrames) if (frameCount >= this.maxFrames)
{ {
goto EOF; goto EOF;
@ -246,9 +248,12 @@ internal sealed class PngDecoderCore : ImageDecoderCore
} }
break; break;
}
case PngChunkType.Data: case PngChunkType.Data:
{
pngMetadata.AnimateRootFrame = currentFrameControl != null; pngMetadata.AnimateRootFrame = currentFrameControl != null;
currentFrameControl ??= new((uint)this.header.Width, (uint)this.header.Height); currentFrameControl ??= new FrameControl((uint)this.header.Width, (uint)this.header.Height);
if (image is null) if (image is null)
{ {
this.InitializeImage(metadata, currentFrameControl.Value, out image); this.InitializeImage(metadata, currentFrameControl.Value, out image);
@ -276,6 +281,8 @@ internal sealed class PngDecoderCore : ImageDecoderCore
} }
break; break;
}
case PngChunkType.Palette: case PngChunkType.Palette:
this.palette = chunk.Data.GetSpan().ToArray(); this.palette = chunk.Data.GetSpan().ToArray();
break; break;
@ -323,6 +330,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
PngThrowHelper.ThrowNoData(); PngThrowHelper.ThrowNoData();
} }
_ = this.TryConvertIccProfile(image);
return image; return image;
} }
catch catch
@ -433,7 +441,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
} }
pngMetadata.AnimateRootFrame = currentFrameControl != null; pngMetadata.AnimateRootFrame = currentFrameControl != null;
currentFrameControl ??= new((uint)this.header.Width, (uint)this.header.Height); currentFrameControl ??= new FrameControl((uint)this.header.Width, (uint)this.header.Height);
if (framesMetadata.Count == 0) if (framesMetadata.Count == 0)
{ {
InitializeFrameMetadata(framesMetadata, currentFrameControl.Value); InitializeFrameMetadata(framesMetadata, currentFrameControl.Value);
@ -525,7 +533,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
PngThrowHelper.ThrowInvalidHeader(); PngThrowHelper.ThrowInvalidHeader();
} }
return new ImageInfo(new(this.header.Width, this.header.Height), metadata, framesMetadata); return new ImageInfo(new Size(this.header.Width, this.header.Height), metadata, framesMetadata);
} }
finally finally
{ {
@ -1343,7 +1351,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
pngMetadata.InterlaceMethod = this.header.InterlaceMethod; pngMetadata.InterlaceMethod = this.header.InterlaceMethod;
this.pngColorType = this.header.ColorType; this.pngColorType = this.header.ColorType;
this.Dimensions = new(this.header.Width, this.header.Height); this.Dimensions = new Size(this.header.Width, this.header.Height);
} }
/// <summary> /// <summary>
@ -1433,14 +1441,19 @@ internal sealed class PngDecoderCore : ImageDecoderCore
/// object unmodified.</returns> /// object unmodified.</returns>
private static bool TryReadTextChunkMetadata(ImageMetadata baseMetadata, string chunkName, string chunkText) private static bool TryReadTextChunkMetadata(ImageMetadata baseMetadata, string chunkName, string chunkText)
{ {
if (chunkName.Equals("Raw profile type exif", StringComparison.OrdinalIgnoreCase) && if (chunkName.Equals(PngConstants.ExifRawProfileKeyword, StringComparison.OrdinalIgnoreCase) &&
TryReadLegacyExifTextChunk(baseMetadata, chunkText)) TryReadLegacyExifTextChunk(baseMetadata, chunkText))
{ {
// Successfully parsed legacy exif data from text // Successfully parsed legacy exif data from text
return true; return true;
} }
// TODO: "Raw profile type iptc", potentially others? if (chunkName.Equals(PngConstants.IptcRawProfileKeyword, StringComparison.OrdinalIgnoreCase) &&
TryReadLegacyIptcTextChunk(baseMetadata, chunkText))
{
// Successfully parsed legacy iptc data from text
return true;
}
// No special chunk data identified // No special chunk data identified
return false; return false;
@ -1564,6 +1577,214 @@ internal sealed class PngDecoderCore : ImageDecoderCore
return true; return true;
} }
/// <summary>
/// Reads iptc data encoded into a text chunk with the name "Raw profile type iptc".
/// This convention is used by ImageMagick/exiftool/exiv2/digiKam and stores a byte-count
/// followed by hex-encoded bytes.
/// </summary>
/// <param name="metadata">The <see cref="ImageMetadata"/> to store the decoded iptc tags into.</param>
/// <param name="data">The contents of the "Raw profile type iptc" text chunk.</param>
private static bool TryReadLegacyIptcTextChunk(ImageMetadata metadata, string data)
{
// Preserve first IPTC found.
if (metadata.IptcProfile != null)
{
return true;
}
ReadOnlySpan<char> dataSpan = data.AsSpan().TrimStart();
// Must start with the "iptc" identifier (case-insensitive).
// Common real-world format (ImageMagick/ExifTool) is:
// "IPTC profile\n <len>\n<hex...>"
if (dataSpan.Length < 4 || !StringEqualsInsensitive(dataSpan[..4], "iptc".AsSpan()))
{
return false;
}
// Skip the remainder of the first line ("IPTC profile", etc).
int firstLineEnd = dataSpan.IndexOf('\n');
if (firstLineEnd < 0)
{
return false;
}
dataSpan = dataSpan[(firstLineEnd + 1)..].TrimStart();
// Next line contains the decimal byte length (often indented).
int dataLengthEnd = dataSpan.IndexOf('\n');
if (dataLengthEnd < 0)
{
return false;
}
int dataLength;
try
{
dataLength = ParseInt32(dataSpan[..dataLengthEnd]);
}
catch
{
return false;
}
if (dataLength <= 0)
{
return false;
}
// Skip to the hex-encoded data.
dataSpan = dataSpan[(dataLengthEnd + 1)..].Trim();
byte[] iptcBlob = new byte[dataLength];
try
{
int written = 0;
for (; written < dataLength;)
{
ReadOnlySpan<char> lineSpan = dataSpan;
int newlineIndex = dataSpan.IndexOf('\n');
if (newlineIndex != -1)
{
lineSpan = dataSpan[..newlineIndex];
}
// Important: handle CRLF and any incidental whitespace.
lineSpan = lineSpan.Trim(); // removes ' ', '\t', '\r', '\n', etc.
if (!lineSpan.IsEmpty)
{
written += HexConverter.HexStringToBytes(lineSpan, iptcBlob.AsSpan()[written..]);
}
if (newlineIndex == -1)
{
break;
}
dataSpan = dataSpan[(newlineIndex + 1)..];
}
if (written != dataLength)
{
return false;
}
}
catch
{
return false;
}
// Prefer IRB extraction if this is Photoshop-style data (8BIM resource blocks).
byte[] iptcPayload = TryExtractIptcFromPhotoshopIrb(iptcBlob, out byte[] extracted)
? extracted
: iptcBlob;
metadata.IptcProfile = new IptcProfile(iptcPayload);
return true;
}
/// <summary>
/// Attempts to extract IPTC metadata from a Photoshop Image Resource Block (IRB) contained within the specified
/// data buffer.
/// </summary>
/// <remarks>This method scans the provided data for a Photoshop IRB block containing IPTC metadata and
/// extracts it if present. The method does not validate the contents of the IPTC data beyond locating the
/// appropriate resource block.</remarks>
/// <param name="data">A read-only span of bytes containing the Photoshop IRB data to search for embedded IPTC metadata.</param>
/// <param name="iptcBytes">When this method returns, contains the extracted IPTC metadata as a byte array if found; otherwise, an undefined
/// value.</param>
/// <returns><see langword="true"/> if IPTC metadata is successfully extracted from the IRB data; otherwise, <see langword="false"/>.</returns>
private static bool TryExtractIptcFromPhotoshopIrb(ReadOnlySpan<byte> data, out byte[] iptcBytes)
{
iptcBytes = default!;
ReadOnlySpan<byte> adobePhotoshop30 = PngConstants.AdobePhotoshop30;
// Some writers include the "Photoshop 3.0\0" header, some store just IRB blocks.
if (data.Length >= adobePhotoshop30.Length && data[..adobePhotoshop30.Length].SequenceEqual(adobePhotoshop30))
{
data = data[adobePhotoshop30.Length..];
}
ReadOnlySpan<byte> eightBim = PngConstants.EightBim;
ushort adobeIptcResourceId = PngConstants.AdobeIptcResourceId;
while (data.Length >= 12)
{
if (!data[..4].SequenceEqual(eightBim))
{
return false;
}
data = data[4..];
// Resource ID (2 bytes, big endian)
if (data.Length < 2)
{
return false;
}
ushort resourceId = (ushort)((data[0] << 8) | data[1]);
data = data[2..];
// Pascal string name (1-byte length, then bytes), padded to even.
if (data.Length < 1)
{
return false;
}
int nameLen = data[0];
int nameFieldLen = 1 + nameLen;
if ((nameFieldLen & 1) != 0)
{
nameFieldLen++; // pad to even
}
if (data.Length < nameFieldLen + 4)
{
return false;
}
data = data[nameFieldLen..];
// Resource data size (4 bytes, big endian)
int size = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3];
data = data[4..];
if (size < 0 || data.Length < size)
{
return false;
}
ReadOnlySpan<byte> payload = data[..size];
// Data is padded to even.
int advance = size;
if ((advance & 1) != 0)
{
advance++;
}
if (resourceId == adobeIptcResourceId)
{
iptcBytes = payload.ToArray();
return true;
}
if (data.Length < advance)
{
return false;
}
data = data[advance..];
}
return false;
}
/// <summary> /// <summary>
/// Reads the color profile chunk. The data is stored similar to the zTXt chunk. /// Reads the color profile chunk. The data is stored similar to the zTXt chunk.
/// </summary> /// </summary>

2
src/ImageSharp/Formats/Png/PngDecoderOptions.cs

@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Png;
public sealed class PngDecoderOptions : ISpecializedDecoderOptions public sealed class PngDecoderOptions : ISpecializedDecoderOptions
{ {
/// <inheritdoc/> /// <inheritdoc/>
public DecoderOptions GeneralOptions { get; init; } = new DecoderOptions(); public DecoderOptions GeneralOptions { get; init; } = new();
/// <summary> /// <summary>
/// Gets the maximum memory in bytes that a zTXt, sPLT, iTXt, iCCP, or unknown chunk can occupy when decompressed. /// Gets the maximum memory in bytes that a zTXt, sPLT, iTXt, iCCP, or unknown chunk can occupy when decompressed.

161
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -8,6 +8,7 @@ using System.IO.Hashing;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using System.Text;
using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Chunks;
@ -217,6 +218,7 @@ internal sealed class PngEncoderCore : IDisposable
this.WritePhysicalChunk(stream, metadata); this.WritePhysicalChunk(stream, metadata);
this.WriteExifChunk(stream, metadata); this.WriteExifChunk(stream, metadata);
this.WriteXmpChunk(stream, metadata); this.WriteXmpChunk(stream, metadata);
this.WriteIptcChunk(stream, metadata);
this.WriteTextChunks(stream, pngMetadata); this.WriteTextChunks(stream, pngMetadata);
if (image.Frames.Count > 1) if (image.Frames.Count > 1)
@ -265,7 +267,7 @@ internal sealed class PngEncoderCore : IDisposable
{ {
// Use the previously derived global palette and a shared quantizer to // Use the previously derived global palette and a shared quantizer to
// quantize the subsequent frames. This allows us to cache the color matching resolution. // quantize the subsequent frames. This allows us to cache the color matching resolution.
paletteQuantizer ??= new( paletteQuantizer ??= new PaletteQuantizer<TPixel>(
this.configuration, this.configuration,
this.quantizer!.Options, this.quantizer!.Options,
previousPalette); previousPalette);
@ -889,6 +891,163 @@ internal sealed class PngEncoderCore : IDisposable
this.WriteChunk(stream, PngChunkType.InternationalText, payload); this.WriteChunk(stream, PngChunkType.InternationalText, payload);
} }
/// <summary>
/// Writes the IPTC metadata from the specified image metadata to the provided stream as a compressed zTXt chunk in
/// PNG format, if IPTC data is present.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="meta">The image metadata.</param>
private void WriteIptcChunk(Stream stream, ImageMetadata meta)
{
if ((this.chunkFilter & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks)
{
return;
}
if (meta.IptcProfile is null || !meta.IptcProfile.Values.Any())
{
return;
}
meta.IptcProfile.UpdateData();
byte[]? iptcData = meta.IptcProfile.Data;
if (iptcData?.Length is 0 or null)
{
return;
}
// For interoperability, wrap raw IPTC (IIM) in a Photoshop IRB (8BIM, resource 0x0404),
// since "Raw profile type iptc" commonly stores IRB payloads.
using IMemoryOwner<byte> irb = this.BuildPhotoshopIrbForIptc(iptcData);
Span<byte> irbSpan = irb.GetSpan();
// Build "raw profile" textual wrapper:
// "IPTC profile\n<decimal length>\n<hex bytes...>\n"
string rawProfileText = BuildRawProfileText("IPTC profile", irbSpan);
byte[] compressedData = this.GetZlibCompressedBytes(PngConstants.Encoding.GetBytes(rawProfileText));
// zTXt layout: keyword (latin-1) + 0 + compression-method(0) + compressed-data
const string iptcRawProfileKeyword = PngConstants.IptcRawProfileKeyword;
int payloadLength = iptcRawProfileKeyword.Length + compressedData.Length + 2;
using IMemoryOwner<byte> payload = this.memoryAllocator.Allocate<byte>(payloadLength);
Span<byte> outputBytes = payload.GetSpan();
PngConstants.Encoding.GetBytes(iptcRawProfileKeyword).CopyTo(outputBytes);
int bytesWritten = iptcRawProfileKeyword.Length;
outputBytes[bytesWritten++] = 0; // Null separator
outputBytes[bytesWritten++] = 0; // Compression method: deflate
compressedData.CopyTo(outputBytes[bytesWritten..]);
this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes);
}
/// <summary>
/// Builds a Photoshop Image Resource Block (IRB) containing the specified IPTC-IIM data.
/// </summary>
/// <remarks>The returned IRB uses resource ID 0x0404 and an empty Pascal string for the name, as required
/// for IPTC-NAA record embedding in Photoshop files. The data is padded to ensure even length, as specified by the
/// IRB format.</remarks>
/// <param name="iptcIim">
/// The IPTC-IIM data to embed in the IRB, provided as a read-only span of bytes. The data is included as-is in the
/// resulting block.
/// </param>
/// <returns>
/// A byte array representing the Photoshop IRB with the embedded IPTC-IIM data, formatted according to the
/// Photoshop specification.
/// </returns>
private IMemoryOwner<byte> BuildPhotoshopIrbForIptc(ReadOnlySpan<byte> iptcIim)
{
// IRB block:
// 4 bytes: "8BIM"
// 2 bytes: resource id 0x0404 (big endian)
// 2 bytes: pascal name (len=0) + pad to even => 0x00 0x00
// 4 bytes: data size (big endian)
// n bytes: IPTC-IIM data
// pad to even
int pad = (iptcIim.Length & 1) != 0 ? 1 : 0;
IMemoryOwner<byte> bufferOwner = this.memoryAllocator.Allocate<byte>(4 + 2 + 2 + 4 + iptcIim.Length + pad);
Span<byte> buffer = bufferOwner.GetSpan();
int bytesWritten = 0;
PngConstants.EightBim.CopyTo(buffer);
bytesWritten += 4;
buffer[bytesWritten++] = 0x04;
buffer[bytesWritten++] = 0x04;
buffer[bytesWritten++] = 0x00; // Pascal name length
buffer[bytesWritten++] = 0x00; // pad to even
int size = iptcIim.Length;
buffer[bytesWritten++] = (byte)((size >> 24) & 0xFF);
buffer[bytesWritten++] = (byte)((size >> 16) & 0xFF);
buffer[bytesWritten++] = (byte)((size >> 8) & 0xFF);
buffer[bytesWritten++] = (byte)(size & 0xFF);
iptcIim.CopyTo(buffer[bytesWritten..]);
// Final pad byte already zero-initialized if needed
return bufferOwner;
}
/// <summary>
/// Builds a formatted text representation of a binary profile, including a header, the payload length, and the
/// payload as hexadecimal text.
/// </summary>
/// <remarks>
/// The hexadecimal payload is formatted with 64 bytes per line to improve readability. The
/// output consists of the header line, a line with the payload length, and one or more lines of hexadecimal
/// text.
/// </remarks>
/// <param name="header">The header text to include at the beginning of the profile. This is written as the first line of the output.</param>
/// <param name="payload">The binary payload to encode as hexadecimal text. The payload is split into lines of 64 bytes each.</param>
/// <returns>
/// A string containing the header, the payload length, and the hexadecimal representation of the payload, each on
/// separate lines.
/// </returns>
private static string BuildRawProfileText(string header, ReadOnlySpan<byte> payload)
{
// Hex text can be multi-line
// Use 64 bytes per line (128 hex chars) to keep the chunk readable.
const int bytesPerLine = 64;
int hexChars = payload.Length * 2;
int lineCount = (payload.Length + (bytesPerLine - 1)) / bytesPerLine;
int newlineCount = 2 + lineCount; // header line + length line + hex lines
int capacity = header.Length + 32 + hexChars + newlineCount;
StringBuilder sb = new(capacity);
sb.Append(header).Append('\n');
sb.Append(payload.Length).Append('\n');
int i = 0;
while (i < payload.Length)
{
int take = Math.Min(bytesPerLine, payload.Length - i);
AppendHex(sb, payload.Slice(i, take));
sb.Append('\n');
i += take;
}
return sb.ToString();
}
private static void AppendHex(StringBuilder sb, ReadOnlySpan<byte> data)
{
const string hex = "0123456789ABCDEF";
for (int i = 0; i < data.Length; i++)
{
byte b = data[i];
_ = sb.Append(hex[b >> 4]);
_ = sb.Append(hex[b & 0x0F]);
}
}
/// <summary> /// <summary>
/// Writes the CICP profile chunk /// Writes the CICP profile chunk
/// </summary> /// </summary>

2
src/ImageSharp/Formats/Png/PngFormat.cs

@ -15,7 +15,7 @@ public sealed class PngFormat : IImageFormat<PngMetadata, PngFrameMetadata>
/// <summary> /// <summary>
/// Gets the shared instance. /// Gets the shared instance.
/// </summary> /// </summary>
public static PngFormat Instance { get; } = new PngFormat(); public static PngFormat Instance { get; } = new();
/// <inheritdoc/> /// <inheritdoc/>
public string Name => "PNG"; public string Name => "PNG";

9
src/ImageSharp/Formats/Png/PngFrameMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -31,7 +32,7 @@ public class PngFrameMetadata : IFormatFrameMetadata<PngFrameMetadata>
/// <summary> /// <summary>
/// Gets or sets the frame delay for animated images. /// Gets or sets the frame delay for animated images.
/// If not 0, when utilized in Png animation, this field specifies the number of hundredths (1/100) of a second to /// If not 0, when utilized in Png animation, this field specifies the number of seconds to
/// wait before continuing with the processing of the Data Stream. /// wait before continuing with the processing of the Data Stream.
/// The clock starts ticking immediately after the graphic is rendered. /// The clock starts ticking immediately after the graphic is rendered.
/// </summary> /// </summary>
@ -62,7 +63,7 @@ public class PngFrameMetadata : IFormatFrameMetadata<PngFrameMetadata>
public static PngFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) public static PngFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata)
=> new() => new()
{ {
FrameDelay = new(metadata.Duration.TotalMilliseconds / 1000), FrameDelay = new Rational(metadata.Duration.TotalMilliseconds / 1000),
DisposalMode = GetMode(metadata.DisposalMode), DisposalMode = GetMode(metadata.DisposalMode),
BlendMode = metadata.BlendMode, BlendMode = metadata.BlendMode,
}; };
@ -76,7 +77,7 @@ public class PngFrameMetadata : IFormatFrameMetadata<PngFrameMetadata>
delay = 0; delay = 0;
} }
return new() return new FormatConnectingFrameMetadata
{ {
ColorTableMode = FrameColorTableMode.Global, ColorTableMode = FrameColorTableMode.Global,
Duration = TimeSpan.FromMilliseconds(delay * 1000), Duration = TimeSpan.FromMilliseconds(delay * 1000),
@ -86,7 +87,7 @@ public class PngFrameMetadata : IFormatFrameMetadata<PngFrameMetadata>
} }
/// <inheritdoc/> /// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination) public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
} }

26
src/ImageSharp/Formats/Png/PngMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -110,7 +111,7 @@ public class PngMetadata : IFormatMetadata<PngMetadata>
color = PngColorType.Rgb; color = PngColorType.Rgb;
break; break;
default: default:
if (colorType.HasFlag(PixelColorType.Luminance)) if (colorType.HasFlag(PixelColorType.Luminance | PixelColorType.Alpha))
{ {
color = PngColorType.GrayscaleWithAlpha; color = PngColorType.GrayscaleWithAlpha;
break; break;
@ -129,7 +130,7 @@ public class PngMetadata : IFormatMetadata<PngMetadata>
4 => PngBitDepth.Bit4, 4 => PngBitDepth.Bit4,
_ => (bpc <= 8) ? PngBitDepth.Bit8 : PngBitDepth.Bit16, _ => (bpc <= 8) ? PngBitDepth.Bit8 : PngBitDepth.Bit16,
}; };
return new() return new PngMetadata
{ {
ColorType = color, ColorType = color,
BitDepth = bitDepth, BitDepth = bitDepth,
@ -227,9 +228,26 @@ public class PngMetadata : IFormatMetadata<PngMetadata>
}; };
/// <inheritdoc/> /// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination) public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
=> this.ColorTable = null; {
this.ColorTable = null;
// If the color type is RGB and we have a transparent color, we need to switch to RGBA
// so that we do not incorrectly preserve the obsolete tRNS chunk.
if (this.ColorType == PngColorType.Rgb && this.TransparentColor.HasValue)
{
this.ColorType = PngColorType.RgbWithAlpha;
this.TransparentColor = null;
}
// The same applies for Grayscale.
if (this.ColorType == PngColorType.Grayscale && this.TransparentColor.HasValue)
{
this.ColorType = PngColorType.GrayscaleWithAlpha;
this.TransparentColor = null;
}
}
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();

12
src/ImageSharp/Formats/Png/PngScanlineProcessor.cs

@ -133,7 +133,7 @@ internal static class PngScanlineProcessor
ushort l = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); ushort l = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
ushort a = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2)); ushort a = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2));
Unsafe.Add(ref rowSpanRef, (uint)x) = TPixel.FromLa32(new(l, a)); Unsafe.Add(ref rowSpanRef, (uint)x) = TPixel.FromLa32(new La32(l, a));
} }
} }
else else
@ -143,7 +143,7 @@ internal static class PngScanlineProcessor
{ {
byte l = Unsafe.Add(ref scanlineSpanRef, offset2); byte l = Unsafe.Add(ref scanlineSpanRef, offset2);
byte a = Unsafe.Add(ref scanlineSpanRef, offset2 + bytesPerSample); byte a = Unsafe.Add(ref scanlineSpanRef, offset2 + bytesPerSample);
Unsafe.Add(ref rowSpanRef, x) = TPixel.FromLa16(new(l, a)); Unsafe.Add(ref rowSpanRef, x) = TPixel.FromLa16(new La16(l, a));
offset2 += bytesPerPixel; offset2 += bytesPerPixel;
} }
} }
@ -239,7 +239,7 @@ internal static class PngScanlineProcessor
ushort r = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); ushort r = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
ushort g = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); ushort g = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
ushort b = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); ushort b = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgb48(new(r, g, b)); Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgb48(new Rgb48(r, g, b));
} }
} }
else if (pixelOffset == 0 && increment == 1) else if (pixelOffset == 0 && increment == 1)
@ -258,7 +258,7 @@ internal static class PngScanlineProcessor
byte r = Unsafe.Add(ref scanlineSpanRef, (uint)o); byte r = Unsafe.Add(ref scanlineSpanRef, (uint)o);
byte g = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); byte g = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample));
byte b = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); byte b = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample)));
Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgb24(new(r, g, b)); Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgb24(new Rgb24(r, g, b));
} }
} }
@ -339,7 +339,7 @@ internal static class PngScanlineProcessor
ushort g = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); ushort g = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
ushort b = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); ushort b = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
ushort a = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample)); ushort a = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample));
Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba64(new(r, g, b, a)); Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba64(new Rgba64(r, g, b, a));
} }
} }
else if (pixelOffset == 0 && increment == 1) else if (pixelOffset == 0 && increment == 1)
@ -360,7 +360,7 @@ internal static class PngScanlineProcessor
byte g = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); byte g = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample));
byte b = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); byte b = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample)));
byte a = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (3 * bytesPerSample))); byte a = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (3 * bytesPerSample)));
Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba32(new(r, g, b, a)); Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba32(new Rgba32(r, g, b, a));
} }
} }
} }

4
src/ImageSharp/Formats/Qoi/QoiConstants.cs

@ -18,10 +18,10 @@ internal static class QoiConstants
/// Gets the list of mimetypes that equate to a QOI. /// Gets the list of mimetypes that equate to a QOI.
/// See https://github.com/phoboslab/qoi/issues/167 /// See https://github.com/phoboslab/qoi/issues/167
/// </summary> /// </summary>
public static string[] MimeTypes { get; } = { "image/qoi", "image/x-qoi", "image/vnd.qoi" }; public static string[] MimeTypes { get; } = ["image/qoi", "image/x-qoi", "image/vnd.qoi"];
/// <summary> /// <summary>
/// Gets the list of file extensions that equate to a QOI. /// Gets the list of file extensions that equate to a QOI.
/// </summary> /// </summary>
public static string[] FileExtensions { get; } = { "qoi" }; public static string[] FileExtensions { get; } = ["qoi"];
} }

2
src/ImageSharp/Formats/Qoi/QoiFormat.cs

@ -15,7 +15,7 @@ public sealed class QoiFormat : IImageFormat<QoiMetadata>
/// <summary> /// <summary>
/// Gets the shared instance. /// Gets the shared instance.
/// </summary> /// </summary>
public static QoiFormat Instance { get; } = new QoiFormat(); public static QoiFormat Instance { get; } = new();
/// <inheritdoc/> /// <inheritdoc/>
public string DefaultMimeType => "image/qoi"; public string DefaultMimeType => "image/qoi";

3
src/ImageSharp/Formats/Qoi/QoiMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Qoi; namespace SixLabors.ImageSharp.Formats.Qoi;
@ -89,7 +90,7 @@ public class QoiMetadata : IFormatMetadata<QoiMetadata>
}; };
/// <inheritdoc/> /// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination) public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
} }

4
src/ImageSharp/Formats/Tga/TgaConstants.cs

@ -8,12 +8,12 @@ internal static class TgaConstants
/// <summary> /// <summary>
/// The list of mimetypes that equate to a targa file. /// The list of mimetypes that equate to a targa file.
/// </summary> /// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] { "image/x-tga", "image/x-targa" }; public static readonly IEnumerable<string> MimeTypes = ["image/x-tga", "image/x-targa"];
/// <summary> /// <summary>
/// The list of file extensions that equate to a targa file. /// The list of file extensions that equate to a targa file.
/// </summary> /// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "tga", "vda", "icb", "vst" }; public static readonly IEnumerable<string> FileExtensions = ["tga", "vda", "icb", "vst"];
/// <summary> /// <summary>
/// The file header length of a tga image in bytes. /// The file header length of a tga image in bytes.

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

Loading…
Cancel
Save