Browse Source

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

pull/2793/head
James Jackson-South 4 days 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_range_operator = true:warning
csharp_style_implicit_object_creation_when_type_is_apparent = true:error
# ReSharper inspection severities
resharper_arrange_object_creation_when_type_evident_highlighting = error
resharper_arrange_object_creation_when_type_not_evident_highlighting = error
# "Null" checking preferences
csharp_style_throw_expression = true:warning
csharp_style_conditional_delegate_call = true:warning
@ -174,6 +177,9 @@ csharp_using_directive_placement = outside_namespace:warning
csharp_prefer_static_local_function = true:warning
# Primary constructor preferences
csharp_style_prefer_primary_constructors = false:none
# Collection preferences
dotnet_style_prefer_collection_expression = true:error
resharper_use_collection_expression_highlighting =true:error
##########################################
# Unnecessary Code Rules

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

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

28
.github/workflows/code-coverage.yml

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

6
Directory.Build.props

@ -21,10 +21,14 @@
<!-- Import the shared global .props file -->
<Import Project="$(MSBuildThisFileDirectory)shared-infrastructure\msbuild\props\SixLabors.Global.props" />
<PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net8.0' OR '$(TargetFramework)' == 'net9.0'">
<LangVersion>12.0</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net10.0'">
<LangVersion>14.0</LangVersion>
</PropertyGroup>
<!--
Ensure all custom build configurations based upon "Release" are optimized.
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);
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
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);
var rows = new RowInterval(yMin, yMax);
RowInterval rows = new(yMin, yMax);
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)
where T : struct, IRowOperation
{
var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
IterateRows(rectangle, in parallelSettings, in operation);
}
@ -65,8 +65,8 @@ public static partial class ParallelRowIterator
}
int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps };
var wrappingOperation = new RowOperationWrapper<T>(top, bottom, verticalStep, in operation);
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
RowOperationWrapper<T> wrappingOperation = new(top, bottom, verticalStep, in operation);
Parallel.For(
0,
@ -88,7 +88,7 @@ public static partial class ParallelRowIterator
where T : struct, IRowOperation<TBuffer>
where TBuffer : unmanaged
{
var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
IterateRows<T, TBuffer>(rectangle, in parallelSettings, in operation);
}
@ -135,8 +135,8 @@ public static partial class ParallelRowIterator
}
int verticalStep = DivideCeil(height, numOfSteps);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps };
var wrappingOperation = new RowOperationWrapper<T, TBuffer>(top, bottom, verticalStep, bufferLength, allocator, in operation);
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
RowOperationWrapper<T, TBuffer> wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
Parallel.For(
0,
@ -156,7 +156,7 @@ public static partial class ParallelRowIterator
public static void IterateRowIntervals<T>(Configuration configuration, Rectangle rectangle, in T operation)
where T : struct, IRowIntervalOperation
{
var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
IterateRowIntervals(rectangle, in parallelSettings, in operation);
}
@ -186,14 +186,14 @@ public static partial class ParallelRowIterator
// Avoid TPL overhead in this trivial case:
if (numOfSteps == 1)
{
var rows = new RowInterval(top, bottom);
RowInterval rows = new(top, bottom);
Unsafe.AsRef(in operation).Invoke(in rows);
return;
}
int verticalStep = DivideCeil(rectangle.Height, numOfSteps);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps };
var wrappingOperation = new RowIntervalOperationWrapper<T>(top, bottom, verticalStep, in operation);
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
RowIntervalOperationWrapper<T> wrappingOperation = new(top, bottom, verticalStep, in operation);
Parallel.For(
0,
@ -215,7 +215,7 @@ public static partial class ParallelRowIterator
where T : struct, IRowIntervalOperation<TBuffer>
where TBuffer : unmanaged
{
var parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration);
IterateRowIntervals<T, TBuffer>(rectangle, in parallelSettings, in operation);
}
@ -250,7 +250,7 @@ public static partial class ParallelRowIterator
// Avoid TPL overhead in this trivial case:
if (numOfSteps == 1)
{
var rows = new RowInterval(top, bottom);
RowInterval rows = new(top, bottom);
using IMemoryOwner<TBuffer> buffer = allocator.Allocate<TBuffer>(bufferLength);
Unsafe.AsRef(in operation).Invoke(in rows, buffer.Memory.Span);
@ -259,8 +259,8 @@ public static partial class ParallelRowIterator
}
int verticalStep = DivideCeil(height, numOfSteps);
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = numOfSteps };
var wrappingOperation = new RowIntervalOperationWrapper<T, TBuffer>(top, bottom, verticalStep, bufferLength, allocator, in operation);
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
RowIntervalOperationWrapper<T, TBuffer> wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
Parallel.For(
0,

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

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

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

@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp;
/// </content>
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>
/// 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>
public static ReadOnlyMemory<Color> WernerPalette => WernerPaletteLazy.Value;
private static Color[] CreateWernerPalette() => new[]
{
private static Color[] CreateWernerPalette() =>
[
ParseHex("#f1e9cd"),
ParseHex("#f2e7cf"),
ParseHex("#ece6d0"),
@ -128,5 +128,5 @@ public partial struct Color
ParseHex("#9b856b"),
ParseHex("#766051"),
ParseHex("#453b32")
};
];
}

394
src/ImageSharp/Color/Color.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Globalization;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
@ -81,10 +82,10 @@ public readonly partial struct Color : IEquatable<Color>
PixelTypeInfo info = TPixel.GetPixelTypeInfo();
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>
@ -95,6 +96,21 @@ public readonly partial struct Color : IEquatable<Color>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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>
/// Bulk converts a span of a specified <typeparamref name="TPixel"/> type to a span of <see cref="Color"/>.
/// </summary>
@ -111,81 +127,106 @@ public readonly partial struct Color : IEquatable<Color>
PixelTypeInfo info = TPixel.GetPixelTypeInfo();
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());
}
}
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>
/// Creates a new instance of the <see cref="Color"/> struct
/// from the given hexadecimal string.
/// Gets a <see cref="Color"/> from the given hexadecimal string.
/// </summary>
/// <param name="hex">
/// The hexadecimal representation of the combined color components arranged
/// in rgb, rgba, rrggbb, or rrggbbaa format to match web syntax.
/// The hexadecimal representation of the combined color components.
/// </param>
/// <param name="format">
/// The format of the hexadecimal string to parse, if applicable. Defaults to <see cref="ColorHexFormat.Rgba"/>.
/// </param>
/// <returns>
/// The <see cref="Color"/>.
/// The <see cref="Color"/> equivalent of the hexadecimal input.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Color ParseHex(string hex)
/// <exception cref="ArgumentException">
/// 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);
return FromPixel(rgba);
Guard.NotNull(hex, nameof(hex));
if (!TryParseHex(hex, out Color color, format))
{
throw new ArgumentException("Hexadecimal string is not in the correct format.", nameof(hex));
}
return color;
}
/// <summary>
/// Attempts to creates a new instance of the <see cref="Color"/> struct
/// from the given hexadecimal string.
/// Gets a <see cref="Color"/> from the given hexadecimal string.
/// </summary>
/// <param name="hex">
/// The hexadecimal representation of the combined color components arranged
/// in rgb, rgba, rrggbb, or rrggbbaa format to match web syntax.
/// The hexadecimal representation of the combined color components.
/// </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 name="result">When this method returns, contains the <see cref="Color"/> equivalent of the hexadecimal input.</param>
/// <returns>
/// The <see cref="bool"/>.
/// <see langword="true"/> if the parsing was successful; otherwise, <see langword="false"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryParseHex(string hex, out Color result)
public static bool TryParseHex(string hex, out Color result, ColorHexFormat format = ColorHexFormat.Rgba)
{
result = default;
if (Rgba32.TryParseHex(hex, out Rgba32 rgba))
if (format == ColorHexFormat.Argb)
{
result = FromPixel(rgba);
return true;
if (TryParseArgbHex(hex, out Argb32 argb))
{
result = FromPixel(argb);
return true;
}
}
else if (format == ColorHexFormat.Rgba)
{
if (TryParseRgbaHex(hex, out Rgba32 rgba))
{
result = FromPixel(rgba);
return true;
}
}
return false;
}
/// <summary>
/// Creates a new instance of the <see cref="Color"/> struct
/// from the given input string.
/// Gets a <see cref="Color"/> from the given input string.
/// </summary>
/// <param name="input">
/// The name of the color or the hexadecimal representation of the combined color components arranged
/// in rgb, rgba, rrggbb, or rrggbbaa format to match web syntax.
/// The name of the color or the hexadecimal representation of the combined color components.
/// </param>
/// <param name="format">
/// The format of the hexadecimal string to parse, if applicable. Defaults to <see cref="ColorHexFormat.Rgba"/>.
/// </param>
/// <returns>
/// The <see cref="Color"/>.
/// The <see cref="Color"/> equivalent of the input string.
/// </returns>
/// <exception cref="ArgumentException">Input string is not in the correct format.</exception>
public static Color Parse(string input)
/// <exception cref="ArgumentException">
/// 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));
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));
}
@ -194,18 +235,21 @@ public readonly partial struct Color : IEquatable<Color>
}
/// <summary>
/// Attempts to creates a new instance of the <see cref="Color"/> struct
/// from the given input string.
/// Tries to create a new instance of the <see cref="Color"/> struct from the given input string.
/// </summary>
/// <param name="input">
/// The name of the color or the hexadecimal representation of the combined color components arranged
/// in rgb, rgba, rrggbb, or rrggbbaa format to match web syntax.
/// The name of the color or the hexadecimal representation of the combined color components.
/// </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 name="result">When this method returns, contains the <see cref="Color"/> equivalent of the hexadecimal input.</param>
/// <returns>
/// The <see cref="bool"/>.
/// <see langword="true"/> if the parsing was successful; otherwise, <see langword="false"/>.
/// </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;
@ -219,7 +263,13 @@ public readonly partial struct Color : IEquatable<Color>
return true;
}
return TryParseHex(input, out result);
result = default;
if (string.IsNullOrWhiteSpace(input))
{
return false;
}
return TryParseHex(input, out result, format);
}
/// <summary>
@ -227,6 +277,7 @@ public readonly partial struct Color : IEquatable<Color>
/// </summary>
/// <param name="alpha">The new value of alpha [0..1].</param>
/// <returns>The color having it's alpha channel altered.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Color WithAlpha(float alpha)
{
Vector4 v = this.ToScaledVector4();
@ -235,22 +286,32 @@ public readonly partial struct Color : IEquatable<Color>
}
/// <summary>
/// Gets the hexadecimal representation of the color instance in rrggbbaa form.
/// Gets the hexadecimal string representation of the color instance.
/// </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>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the <paramref name="format"/> is not supported.</exception>
[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 />
public override string ToString() => this.ToHex();
public override string ToString() => this.ToHex(ColorHexFormat.Rgba);
/// <summary>
/// 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();
}
/// <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 xyzr = new(xr, yr, zr);
return new(xyzr * wxyz);
return new CieXyz(xyzr * wxyz);
}
/// <inheritdoc/>

2
src/ImageSharp/ColorProfiles/ColorProfileConverter.cs

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

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabCieXyz.cs

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

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieLabRgb.cs

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

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieLab.cs

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

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzCieXyz.cs

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

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsCieXyzRgb.cs

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

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

192
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs

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

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieLab.cs

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

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbCieXyz.cs

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

32
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsRgbRgb.cs

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

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

@ -58,7 +58,7 @@ internal class ColorTrcCalculator : IVector4Calculator
// when data to PCS, upstream process provides scaled XYZ
// but input to calculator is descaled XYZ
// (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);
}
}

8
src/ImageSharp/ColorProfiles/KnownIlluminants.cs

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

184
src/ImageSharp/ColorProfiles/Rgb.cs

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

2
src/ImageSharp/ColorProfiles/VonKriesChromaticAdaptation.cs

@ -31,7 +31,7 @@ public static class VonKriesChromaticAdaptation
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);

2
src/ImageSharp/ColorProfiles/Y.cs

@ -92,7 +92,7 @@ public readonly struct Y : IColorProfile<Y, Rgb>
{
Matrix4x4 m = options.YCbCrTransform.Forward;
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/>

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.
// 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, // 31
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, // 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, // 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];
}

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

@ -470,8 +470,8 @@ internal static class Numerics
where T : unmanaged
{
ref T sRef = ref MemoryMarshal.GetReference(span);
var vmin = new Vector<T>(min);
var vmax = new Vector<T>(max);
Vector<T> vmin = new(min);
Vector<T> vmax = new(max);
nint n = (nint)(uint)span.Length / Vector<T>.Count;
nint m = Modulo4(n);
@ -656,7 +656,7 @@ internal static class Numerics
return Sse.Shuffle(value.AsVector128(), value.AsVector128(), ShuffleAlphaControl).AsVector4();
}
return new(value.W);
return new Vector4(value.W);
}
/// <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> 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_one = Vector128.Create(1.0f).AsInt32();
var v128_13rd = Vector128.Create(1 / 3f);
var v128_23rds = Vector128.Create(2 / 3f);
Vector128<float> v128_13rd = Vector128.Create(1 / 3f);
Vector128<float> v128_23rds = Vector128.Create(2 / 3f);
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.
/// Does not necessarily fit all use cases!
/// </summary>
public static readonly TolerantMath Default = new TolerantMath(1e-8);
public static readonly TolerantMath Default = new(1e-8);
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.
// <auto-generated />

2
src/ImageSharp/Common/InlineArray.tt

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

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

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

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

@ -124,25 +124,25 @@ internal static class DeflaterConstants
/// <summary>
/// Internal compression engine constant
/// </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>
/// Internal compression engine constant
/// </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>
/// Internal compression engine constant
/// </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>
/// Internal compression engine constant
/// </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>
/// Internal compression engine constant
/// </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
// 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,
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,
@ -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,
0, 64, 32, 96, 16, 80, 48, 112, 8, 72, 40, 104, 24, 88, 56, 120, 4, 68, 36,
100, 20, 84, 52, 116, 3, 131, 67, 195, 35, 163
};
];
private static ReadOnlySpan<byte> StaticLLength => 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,
@ -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,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8
};
];
// Distance codes and lengths.
private static readonly short[] StaticDCodes = new short[]
{
private static readonly short[] StaticDCodes =
[
0, 16, 8, 24, 4, 20, 12, 28, 2, 18, 10, 26, 6, 22, 14,
30, 1, 17, 9, 25, 5, 21, 13, 29, 3, 19, 11, 27, 7, 23
};
];
private static ReadOnlySpan<byte> StaticDLength => 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
};
];
#pragma warning restore SA1201 // Elements should appear in the correct order
/// <summary>
/// Gets the lengths of the bit length codes are sent in order of decreasing probability, to avoid transmitting the lengths for unused bit length codes.
/// </summary>
private static ReadOnlySpan<byte> BitLengthOrder => 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
};
];
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
};
];
/// <summary>
/// Gets the pending buffer to use.

2
src/ImageSharp/Configuration.cs

@ -122,7 +122,7 @@ public sealed class Configuration
/// <summary>
/// Gets or the <see cref="ImageFormatManager"/> that is currently in use.
/// </summary>
public ImageFormatManager ImageFormatsManager { get; private set; } = new ImageFormatManager();
public ImageFormatManager ImageFormatsManager { get; private set; } = new();
/// <summary>
/// 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>
/// The list of mimetypes that equate to a bmp.
/// </summary>
public static readonly IEnumerable<string> MimeTypes = new[]
{
public static readonly IEnumerable<string> MimeTypes =
[
"image/bmp",
"image/x-windows-bmp",
"image/x-win-bitmap"
};
];
/// <summary>
/// The list of file extensions that equate to a bmp.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "bm", "bmp", "dip" };
public static readonly IEnumerable<string> FileExtensions = ["bm", "bmp", "dip"];
/// <summary>
/// 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(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/>

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

@ -220,7 +220,7 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
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>
@ -1270,7 +1270,7 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
byte g = (byte)((temp & greenMask) >> rightShiftGreenMask);
byte b = (byte)((temp & blueMask) >> rightShiftBlueMask);
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;
@ -1457,7 +1457,7 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
this.bmpMetadata.InfoHeaderType = infoHeaderType;
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>

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

@ -655,7 +655,7 @@ internal sealed class BmpEncoderCore
CancellationToken cancellationToken)
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,
Dither = this.quantizer.Options.Dither,
@ -712,7 +712,7 @@ internal sealed class BmpEncoderCore
CancellationToken cancellationToken)
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,
Dither = this.quantizer.Options.Dither,
@ -778,7 +778,7 @@ internal sealed class BmpEncoderCore
CancellationToken cancellationToken)
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,
Dither = this.quantizer.Options.Dither,

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

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

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

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

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

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

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

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

10
src/ImageSharp/Formats/DecoderOptions.cs

@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Formats;
/// </summary>
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;
@ -78,12 +78,12 @@ public sealed class DecoderOptions
return false;
}
if (IccProfileHeader.IsLikelySrgb(profile.Header))
if (this.ColorProfileHandling == ColorProfileHandling.Preserve)
{
return false;
}
if (this.ColorProfileHandling == ColorProfileHandling.Preserve)
if (profile.IsCanonicalSrgbMatrixTrc())
{
return false;
}
@ -99,11 +99,11 @@ public sealed class DecoderOptions
return false;
}
if (this.ColorProfileHandling == ColorProfileHandling.Compact && IccProfileHeader.IsLikelySrgb(profile.Header))
if (this.ColorProfileHandling == ColorProfileHandling.Convert)
{
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>
/// The collection of mimetypes that equate to a Gif.
/// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] { "image/gif" };
public static readonly IEnumerable<string> MimeTypes = ["image/gif"];
/// <summary>
/// The collection of file extensions that equate to a Gif.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "gif" };
public static readonly IEnumerable<string> FileExtensions = ["gif"];
/// <summary>
/// Gets the ASCII encoded bytes used to identify the GIF file (combining <see cref="FileType"/> and <see cref="FileVersion"/>).
/// </summary>
internal static ReadOnlySpan<byte> MagicNumber => new[]
{
internal static ReadOnlySpan<byte> MagicNumber =>
[
(byte)'G', (byte)'I', (byte)'F',
(byte)'8', (byte)'9', (byte)'a'
};
];
/// <summary>
/// Gets the ASCII encoded application identification bytes (representing <see cref="NetscapeApplicationIdentification"/>).
/// </summary>
internal static ReadOnlySpan<byte> NetscapeApplicationIdentificationBytes => new[]
{
internal static ReadOnlySpan<byte> NetscapeApplicationIdentificationBytes =>
[
(byte)'N', (byte)'E', (byte)'T',
(byte)'S', (byte)'C', (byte)'A',
(byte)'P', (byte)'E',
(byte)'2', (byte)'.', (byte)'0'
};
];
/// <summary>
/// Gets the ASCII encoded application identification bytes.
/// </summary>
internal static ReadOnlySpan<byte> XmpApplicationIdentificationBytes => new[]
{
internal static ReadOnlySpan<byte> XmpApplicationIdentificationBytes =>
[
(byte)'X', (byte)'M', (byte)'P',
(byte)' ', (byte)'D', (byte)'a',
(byte)'t', (byte)'a',
(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(
new(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height),
new Size(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height),
this.metadata,
framesMetadata);
}
@ -305,7 +305,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore
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>
@ -414,6 +414,11 @@ internal sealed class GifDecoderCore : ImageDecoderCore
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)
{
stream.Seek(length, SeekOrigin.Current);
@ -484,6 +489,12 @@ internal sealed class GifDecoderCore : ImageDecoderCore
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)
{
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>());
// 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)
{
backgroundColor = Color.FromPixel(colorTable[this.backgroundColorIndex]);
}
else
{
backgroundColor = 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);
}
// 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
// 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
// color is display advice and should be used only when explicitly flattening or when
// rendering with an option to honor it.
backgroundColor = (this.backgroundColorIndex < colorTable.Length)
? Color.FromPixel(colorTable[this.backgroundColorIndex]).WithAlpha(0)
: Color.Transparent;
}
// Skip any remaining blocks
@ -541,30 +546,45 @@ internal sealed class GifDecoderCore : ImageDecoderCore
GifImageDescriptor descriptor = this.imageDescriptor;
int imageWidth = this.logicalScreenDescriptor.Width;
int imageHeight = this.logicalScreenDescriptor.Height;
bool transFlag = this.graphicsControlExtension.TransparencyFlag;
bool useTransparency = this.graphicsControlExtension.TransparencyFlag;
bool useBackground;
FrameDisposalMode disposalMethod = this.graphicsControlExtension.DisposalMethod;
ImageFrame<TPixel> currentFrame;
ImageFrame<TPixel>? restoreFrame = null;
if (previousFrame is null && previousDisposalMode is null)
{
image = transFlag
? new Image<TPixel>(this.configuration, imageWidth, imageHeight, this.metadata)
: new Image<TPixel>(this.configuration, imageWidth, imageHeight, backgroundPixel, this.metadata);
// First frame: prefill with LSD background iff a GCT exists (policy: HonorBackgroundColor).
useBackground =
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);
currentFrame = image.Frames.RootFrame;
}
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)
{
currentFrame = image!.Frames.AddFrame(previousFrame);
}
else
else if (useBackground)
{
currentFrame = image!.Frames.CreateFrame(backgroundPixel);
}
else
{
currentFrame = image!.Frames.CreateFrame();
}
this.SetFrameMetadata(currentFrame.Metadata);
@ -575,7 +595,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore
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)
{
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)
@ -665,12 +685,18 @@ internal sealed class GifDecoderCore : ImageDecoderCore
// Take the descriptorLeft..maxX slice of the row, so the loop can be simplified.
row = row[descriptorLeft..maxX];
if (!transFlag)
if (!useTransparency)
{
for (int x = 0; x < row.Length; 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]);
}
}
@ -681,6 +707,8 @@ internal sealed class GifDecoderCore : ImageDecoderCore
int index = indicesRow[x];
// 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)
{
continue;

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

@ -113,18 +113,22 @@ internal sealed class GifEncoderCore
TransparentColorMode mode = this.transparentColorMode;
// 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)
{
// Is this a gif with color information. If so use that, otherwise use octree.
if (gifMetadata.ColorTableMode == FrameColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0)
{
int transparencyIndex = GetTransparentIndex(quantized, frameMetadata);
if (transparencyIndex >= 0 || gifMetadata.GlobalColorTable.Value.Length < 256)
int ti = GetTransparentIndex(quantized, frameMetadata);
if (ti >= 0 || gifMetadata.GlobalColorTable.Value.Length < 256)
{
// 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
{
@ -173,20 +177,14 @@ internal sealed class GifEncoderCore
WriteHeader(stream);
// Write the LSD.
int derivedTransparencyIndex = GetTransparentIndex(quantized, null);
if (derivedTransparencyIndex >= 0)
int transparencyIndex = GetTransparentIndex(quantized, null);
if (transparencyIndex >= 0)
{
frameMetadata.HasTransparency = true;
frameMetadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex);
frameMetadata.TransparencyIndex = ClampIndex(transparencyIndex);
}
// TODO: We should be checking the metadata here also I think?
if (!TryGetBackgroundIndex(quantized, this.backgroundColor, out byte backgroundIndex))
{
backgroundIndex = derivedTransparencyIndex >= 0
? frameMetadata.TransparencyIndex
: gifMetadata.BackgroundColorIndex;
}
byte backgroundIndex = GetBackgroundIndex(quantized, gifMetadata, this.backgroundColor);
// Get the number of bits.
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
@ -224,7 +222,7 @@ internal sealed class GifEncoderCore
image,
globalQuantizer,
globalFrameQuantizer,
derivedTransparencyIndex,
transparencyIndex,
frameMetadata.DisposalMode,
cancellationToken);
}
@ -334,15 +332,23 @@ internal sealed class GifEncoderCore
{
// Capture any explicit transparency index from the metadata.
// 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
? null :
previousFrame;
Color background = metadata.DisposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
: Color.Transparent;
// If the previous frame has a value we need to check the disposal mode of that frame
// to determine if we should use the background color to fill the encoding frame
// 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.
(bool difference, Rectangle bounds) =
@ -491,6 +497,7 @@ internal sealed class GifEncoderCore
return quantized;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static byte ClampIndex(int value) => (byte)Numerics.Clamp(value, byte.MinValue, byte.MaxValue);
/// <summary>
@ -531,41 +538,38 @@ internal sealed class GifEncoderCore
/// Returns the index of the background color in the palette.
/// </summary>
/// <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="index">The index in the palette of the background color.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>The <see cref="bool"/>.</returns>
private static bool TryGetBackgroundIndex<TPixel>(
IndexedImageFrame<TPixel>? quantized,
Color? background,
out byte index)
/// <returns>The <see cref="byte"/> index of the background color.</returns>
private static byte GetBackgroundIndex<TPixel>(IndexedImageFrame<TPixel>? quantized, GifMetadata metadata, Color? background)
where TPixel : unmanaged, IPixel<TPixel>
{
int match = -1;
if (quantized != null && background.HasValue)
if (quantized != null)
{
TPixel backgroundPixel = background.Value.ToPixel<TPixel>();
ReadOnlySpan<TPixel> palette = quantized.Palette.Span;
for (int i = 0; i < palette.Length; i++)
if (background.HasValue)
{
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;
break;
match = i;
break;
}
}
else if (metadata.BackgroundColorIndex < quantized.Palette.Length)
{
match = metadata.BackgroundColorIndex;
}
}
if (match >= 0)
{
index = (byte)Numerics.Clamp(match, 0, 255);
return true;
}
index = 0;
return false;
return ClampIndex(match);
}
/// <summary>

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

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
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.
blendSource |= this.ColorTableMode == FrameColorTableMode.Global && !this.HasTransparency;
return new()
return new FormatConnectingFrameMetadata
{
ColorTableMode = this.ColorTableMode,
Duration = TimeSpan.FromMilliseconds(this.FrameDelay * 10),
@ -103,7 +104,7 @@ public class GifFrameMetadata : IFormatFrameMetadata<GifFrameMetadata>
}
/// <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>
=> this.LocalColorTable = null;

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

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Gif;
@ -105,7 +106,7 @@ public class GifMetadata : IFormatMetadata<GifMetadata>
};
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
=> 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
/// </summary>
private static readonly int[] Masks =
{
[
0b0,
0b1,
0b11,
@ -65,7 +65,7 @@ internal sealed class LzwEncoder : IDisposable
0b11111111111111,
0b111111111111111,
0b1111111111111111
};
];
/// <summary>
/// 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
int xmpLength = xmpBytes.Length - 256; // 257 - unread 0x0
byte[] buffer = Array.Empty<byte>();
byte[] buffer = [];
if (xmpLength > 0)
{
buffer = new byte[xmpLength];

4
src/ImageSharp/Formats/IFormatFrameMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats;
@ -22,7 +23,8 @@ public interface IFormatFrameMetadata : IDeepCloneable
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <param name="source">The source 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>;
}

4
src/ImageSharp/Formats/IFormatMetadata.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats;
@ -27,7 +28,8 @@ public interface IFormatMetadata : IDeepCloneable
/// </summary>
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
/// <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>;
}

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

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

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

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.PixelFormats;
@ -148,7 +149,7 @@ public class IcoMetadata : IFormatMetadata<IcoMetadata>
};
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
=> 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
// 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();
@ -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
// 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.
@ -222,7 +222,7 @@ internal abstract class IconDecoderCore : ImageDecoderCore
metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata);
}
return new(this.Dimensions, metadata, frames);
return new ImageInfo(this.Dimensions, metadata, frames);
}
protected abstract void SetFrameMetadata(
@ -276,20 +276,20 @@ internal abstract class IconDecoderCore : ImageDecoderCore
height = Math.Max(height, entry.Height);
}
this.Dimensions = new(width, height);
this.Dimensions = new Size(width, height);
}
private ImageDecoderCore GetDecoder(bool isPng)
{
if (isPng)
{
return new PngDecoderCore(new()
return new PngDecoderCore(new PngDecoderOptions
{
GeneralOptions = this.Options,
});
}
return new BmpDecoderCore(new()
return new BmpDecoderCore(new BmpDecoderOptions
{
GeneralOptions = this.Options,
ProcessedAlphaMask = true,

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

@ -116,7 +116,7 @@ internal abstract class IconEncoderCore
[MemberNotNull(nameof(entries))]
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
{
IconFileType.ICO =>
@ -155,14 +155,14 @@ internal abstract class IconEncoderCore
count = 256;
}
return new WuQuantizer(new()
return new WuQuantizer(new QuantizerOptions
{
MaxColors = count
});
}
// 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

16
src/ImageSharp/Formats/ImageDecoder.cs

@ -328,6 +328,14 @@ public abstract class ImageDecoder : IImageDecoder
{
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)
@ -336,5 +344,13 @@ public abstract class ImageDecoder : IImageDecoder
{
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.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.ColorProfiles;
using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats;
@ -124,4 +128,93 @@ internal abstract class ImageDecoderCore
/// </remarks>
protected abstract Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
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>
/// Removes all the registered image format detectors.
/// </summary>
public void ClearImageFormatDetectors() => this.imageFormatDetectors = new();
public void ClearImageFormatDetectors() => this.imageFormatDetectors = new ConcurrentBag<IImageFormatDetector>();
/// <summary>
/// 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 />
public override string ToString()
{
var sb = new StringBuilder();
StringBuilder sb = new();
sb.Append('[');
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 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.W = sLeft.Y;
var zwLeft = new Vector4(sLeft.Z);
Vector4 zwLeft = new(sLeft.Z);
zwLeft.Z = sLeft.W;
zwLeft.W = sLeft.W;
var xyRight = new Vector4(sRight.X);
Vector4 xyRight = new(sRight.X);
xyRight.Z = sRight.Y;
xyRight.W = sRight.Y;
var zwRight = new Vector4(sRight.Z);
Vector4 zwRight = new(sRight.Z);
zwRight.Z = 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();
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.
// This transform assumes Rec.601 YCbCr coefficients and an inverted K channel.
//
@ -144,7 +144,7 @@ internal abstract partial class JpegColorConverterBase
SourceIccProfile = profile,
TargetIccProfile = CompactSrgbV4Profile.Profile,
};
converter = new(options);
converter = new ColorProfileConverter(options);
converter.Convert<Cmyk, Rgb>(source, destination);
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,
TargetIccProfile = CompactSrgbV4Profile.Profile,
};
converter = new(options);
converter = new ColorProfileConverter(options);
converter.Convert<Rgb, Rgb>(destination, destination);
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,
TargetIccProfile = CompactSrgbV4Profile.Profile,
};
converter = new(options);
converter = new ColorProfileConverter(options);
converter.Convert<Cmyk, Rgb>(source, destination);
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>
/// Gets the JFIF specific markers.
/// </summary>
public static ReadOnlySpan<byte> JFifMarker => new[]
{
public static ReadOnlySpan<byte> JFifMarker =>
[
(byte)'J', (byte)'F', (byte)'I', (byte)'F', (byte)'\0'
};
];
/// <summary>
/// Gets the JFXX specific markers.
/// </summary>
public static ReadOnlySpan<byte> JFxxMarker => new[]
{
public static ReadOnlySpan<byte> JFxxMarker =>
[
(byte)'J', (byte)'F', (byte)'X', (byte)'X', (byte)'\0'
};
];
/// <summary>
/// Gets the ICC specific markers.
/// </summary>
public static ReadOnlySpan<byte> IccMarker => new[]
{
public static ReadOnlySpan<byte> IccMarker =>
[
(byte)'I', (byte)'C', (byte)'C', (byte)'_',
(byte)'P', (byte)'R', (byte)'O', (byte)'F',
(byte)'I', (byte)'L', (byte)'E', (byte)'\0'
};
];
/// <summary>
/// Gets the adobe photoshop APP13 marker which can contain IPTC meta data.
/// </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'
};
];
/// <summary>
/// Gets the 8BIM marker, which signals the start of a adobe specific image resource block.
/// </summary>
public static ReadOnlySpan<byte> AdobeImageResourceBlockMarker => new[]
{
public static ReadOnlySpan<byte> AdobeImageResourceBlockMarker =>
[
(byte)'8', (byte)'B', (byte)'I', (byte)'M'
};
];
/// <summary>
/// Gets a IPTC Image resource ID.
/// </summary>
public static ReadOnlySpan<byte> AdobeIptcMarker => new[]
{
public static ReadOnlySpan<byte> AdobeIptcMarker =>
[
(byte)4, (byte)4
};
];
/// <summary>
/// Gets the EXIF specific markers.
/// </summary>
public static ReadOnlySpan<byte> ExifMarker => new[]
{
public static ReadOnlySpan<byte> ExifMarker =>
[
(byte)'E', (byte)'x', (byte)'i', (byte)'f', (byte)'\0', (byte)'\0'
};
];
/// <summary>
/// Gets the XMP specific markers.
/// </summary>
public static ReadOnlySpan<byte> XmpMarker => new[]
{
public static ReadOnlySpan<byte> XmpMarker =>
[
(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)'e', (byte)'.', (byte)'c', (byte)'o', (byte)'m', (byte)'/', (byte)'x',
(byte)'a', (byte)'p', (byte)'/', (byte)'1', (byte)'.', (byte)'0', (byte)'/',
(byte)0
};
];
/// <summary>
/// Gets the Adobe specific markers <see href="http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html#Adobe"/>.
/// </summary>
public static ReadOnlySpan<byte> AdobeMarker => new[]
{
public static ReadOnlySpan<byte> AdobeMarker =>
[
(byte)'A', (byte)'d', (byte)'o', (byte)'b', (byte)'e'
};
];
/// <summary>
/// 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));
nuint count = target.VectorCount<float>();
var multiplierVector = new Vector<float>(multiplier);
Vector<float> multiplierVector = new(multiplier);
for (nuint i = 0; i < count; i++)
{
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.WriteRestart(restarts % 8);
foreach (var component in frame.Components)
foreach (Component component in frame.Components)
{
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.
/// </remarks>
public static readonly HuffmanSpec LuminanceDC = new(
new byte[]
{
[
0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0
},
new byte[]
{
],
[
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
});
]);
/// <summary>
/// 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.
/// </remarks>
public static readonly HuffmanSpec LuminanceAC = new(
new byte[]
{
[
0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0,
0, 1, 125
},
new byte[]
{
],
[
0x01, 0x02, 0x03, 0x00, 0x04, 0x11,
0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13,
0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32,
@ -63,7 +59,7 @@ internal readonly struct HuffmanSpec
0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1,
0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
0xf9, 0xfa
});
]);
/// <summary>
/// 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.
/// </remarks>
public static readonly HuffmanSpec ChrominanceDC = new(
new byte[]
{
[
0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 0
},
new byte[]
{
],
[
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
});
]);
/// <summary>
/// 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.
/// </remarks>
public static readonly HuffmanSpec ChrominanceAC = new(
new byte[]
{
[
0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0,
1, 2, 119
},
new byte[]
{
],
[
0x00, 0x01, 0x02, 0x03, 0x11, 0x04,
0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51,
0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81,
@ -120,7 +112,7 @@ internal readonly struct HuffmanSpec
0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,
0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
0xf9, 0xfa
});
]);
/// <summary>
/// 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
const int blockPixelWidth = 8;
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];
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]);
// 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);
}

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

@ -49,8 +49,8 @@ internal static partial class FloatingPointDCT
/// </code>
/// </para>
/// </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,
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,
@ -58,8 +58,8 @@ internal static partial class FloatingPointDCT
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.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>
/// 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.
// This is effectively compiled down to: return new ReadOnlySpan<byte>(&data, length)
// 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,
12, 12, 14, 19, 26, 58, 60, 55,
14, 13, 16, 24, 40, 57, 69, 56,
@ -55,8 +55,8 @@ internal static class Quantization
18, 22, 37, 56, 68, 109, 103, 77,
24, 35, 55, 64, 81, 104, 113, 92,
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>
/// 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.
// This is effectively compiled down to: return new ReadOnlySpan<byte>(&data, length)
// 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,
18, 21, 26, 66, 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
];
/// Ported from JPEGsnoop:
/// 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'.
/// TODO: Shouldn't we expose this as operator in SixLabors.Core?
/// </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>
/// Divides 'a.Width' with 'b.Width' and 'a.Height' with 'b.Height'.
/// TODO: Shouldn't we expose this as operator in SixLabors.Core?
/// </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>
/// Divide Width and Height as real numbers and return the Ceiling.
/// </summary>
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.X = MathF.Ceiling(sizeVect.X);
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
/// fake entries.
/// </remarks>
public static ReadOnlySpan<byte> ZigZagOrder => new byte[]
{
public static ReadOnlySpan<byte> ZigZagOrder =>
[
0, 1, 8, 16, 9, 2, 3, 10,
17, 24, 32, 25, 18, 11, 4, 5,
12, 19, 26, 33, 40, 48, 41, 34,
@ -32,7 +32,7 @@ internal static partial class ZigZag
// Extra entries for safety in decoder
63, 63, 63, 63, 63, 63, 63, 63,
63, 63, 63, 63, 63, 63, 63, 63
};
];
/// <summary>
/// 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
/// fake entries.
/// </remarks>
public static ReadOnlySpan<byte> TransposingOrder => new byte[]
{
public static ReadOnlySpan<byte> TransposingOrder =>
[
0, 8, 1, 2, 9, 16, 24, 17,
10, 3, 4, 11, 18, 25, 32, 40,
33, 26, 19, 12, 5, 6, 13, 20,
@ -61,5 +61,5 @@ internal static partial class ZigZag
// Extra entries for safety in decoder
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>
/// The list of mimetypes that equate to a jpeg.
/// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] { "image/jpeg", "image/pjpeg" };
public static readonly IEnumerable<string> MimeTypes = ["image/jpeg", "image/pjpeg"];
/// <summary>
/// The list of file extensions that equate to a jpeg.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "jpg", "jpeg", "jfif" };
public static readonly IEnumerable<string> FileExtensions = ["jpg", "jpeg", "jfif"];
/// <summary>
/// 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(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);
}

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

@ -71,6 +71,11 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
/// </summary>
private bool hasAdobeMarker;
/// <summary>
/// Whether the image has a SOS marker.
/// </summary>
private bool hasSOSMarker;
/// <summary>
/// Contains information about the JFIF marker.
/// </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);
this.ParseStream(stream, spectralConverter, cancellationToken);
if (!this.hasSOSMarker)
{
JpegThrowHelper.ThrowInvalidImageContentException("Missing SOS marker.");
}
this.InitExifProfile();
this.InitIccProfile();
this.InitIptcProfile();
@ -215,6 +226,12 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.ParseStream(stream, spectralConverter: null, cancellationToken);
if (!this.hasSOSMarker)
{
JpegThrowHelper.ThrowInvalidImageContentException("Missing SOS marker.");
}
this.InitExifProfile();
this.InitIccProfile();
this.InitIptcProfile();
@ -222,7 +239,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
this.InitDerivedMetadataProperties();
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>
@ -403,6 +420,8 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
break;
case JpegConstants.Markers.SOS:
this.hasSOSMarker = true;
if (!metadataOnly)
{
this.ProcessStartOfScanMarker(stream, markerContentByteSize);
@ -1243,7 +1262,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
}
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;
remaining -= length;

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

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

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

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

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

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

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

@ -16,10 +16,10 @@ internal static class PbmConstants
/// <summary>
/// The list of mimetypes that equate to a ppm.
/// </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>
/// The list of file extensions that equate to a ppm.
/// </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);
return new ImageInfo(
new(this.pixelSize.Width, this.pixelSize.Height),
new Size(this.pixelSize.Width, this.pixelSize.Height),
this.metadata);
}

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

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

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

@ -13,22 +13,22 @@ internal static class Adam7
/// <summary>
/// The amount to increment when processing each column per scanline for each interlaced pass.
/// </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>
/// The index to start at when processing each column per scanline for each interlaced pass.
/// </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>
/// The index to start at when processing each row per scanline for each interlaced pass.
/// </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>
/// The amount to increment when processing each row per scanline for each interlaced pass.
/// </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>
/// Gets the width of the block.

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

@ -28,12 +28,12 @@ internal static class PngConstants
/// <summary>
/// The list of mimetypes that equate to a Png.
/// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] { "image/png", "image/apng" };
public static readonly IEnumerable<string> MimeTypes = ["image/png", "image/apng"];
/// <summary>
/// The list of file extensions that equate to a Png.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "png", "apng" };
public static readonly IEnumerable<string> FileExtensions = ["png", "apng"];
/// <summary>
/// The header bytes as a big-endian coded ulong.
@ -45,11 +45,11 @@ internal static class PngConstants
/// </summary>
public static readonly Dictionary<PngColorType, byte[]> ColorTypes = new()
{
[PngColorType.Grayscale] = new byte[] { 1, 2, 4, 8, 16 },
[PngColorType.Rgb] = new byte[] { 8, 16 },
[PngColorType.Palette] = new byte[] { 1, 2, 4, 8 },
[PngColorType.GrayscaleWithAlpha] = new byte[] { 8, 16 },
[PngColorType.RgbWithAlpha] = new byte[] { 8, 16 }
[PngColorType.Grayscale] = [1, 2, 4, 8, 16],
[PngColorType.Rgb] = [8, 16],
[PngColorType.Palette] = [1, 2, 4, 8],
[PngColorType.GrayscaleWithAlpha] = [8, 16],
[PngColorType.RgbWithAlpha] = [8, 16]
};
/// <summary>
@ -62,11 +62,26 @@ internal static class PngConstants
/// </summary>
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>
/// Gets the header bytes identifying a Png.
/// </summary>
public static ReadOnlySpan<byte> HeaderBytes => new byte[]
{
public static ReadOnlySpan<byte> HeaderBytes =>
[
0x89, // Set the high bit.
0x50, // P
0x4E, // N
@ -75,13 +90,13 @@ internal static class PngConstants
0x0A, // Line ending CRLF
0x1A, // EOF
0x0A // LF
};
];
/// <summary>
/// Gets the keyword of the XMP metadata, encoded in an iTXT chunk.
/// </summary>
public static ReadOnlySpan<byte> XmpKeyword => new[]
{
public static ReadOnlySpan<byte> XmpKeyword =>
[
(byte)'X',
(byte)'M',
(byte)'L',
@ -99,5 +114,32 @@ internal static class PngConstants
(byte)'x',
(byte)'m',
(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(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/>

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.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
@ -212,6 +213,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
currentFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan());
break;
case PngChunkType.FrameData:
{
if (frameCount >= this.maxFrames)
{
goto EOF;
@ -246,9 +248,12 @@ internal sealed class PngDecoderCore : ImageDecoderCore
}
break;
}
case PngChunkType.Data:
{
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)
{
this.InitializeImage(metadata, currentFrameControl.Value, out image);
@ -276,6 +281,8 @@ internal sealed class PngDecoderCore : ImageDecoderCore
}
break;
}
case PngChunkType.Palette:
this.palette = chunk.Data.GetSpan().ToArray();
break;
@ -323,6 +330,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
PngThrowHelper.ThrowNoData();
}
_ = this.TryConvertIccProfile(image);
return image;
}
catch
@ -433,7 +441,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
}
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)
{
InitializeFrameMetadata(framesMetadata, currentFrameControl.Value);
@ -525,7 +533,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
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
{
@ -1343,7 +1351,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
pngMetadata.InterlaceMethod = this.header.InterlaceMethod;
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>
@ -1433,14 +1441,19 @@ internal sealed class PngDecoderCore : ImageDecoderCore
/// object unmodified.</returns>
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))
{
// Successfully parsed legacy exif data from text
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
return false;
@ -1564,6 +1577,214 @@ internal sealed class PngDecoderCore : ImageDecoderCore
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>
/// Reads the color profile chunk. The data is stored similar to the zTXt chunk.
/// </summary>

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

@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Png;
public sealed class PngDecoderOptions : ISpecializedDecoderOptions
{
/// <inheritdoc/>
public DecoderOptions GeneralOptions { get; init; } = new DecoderOptions();
public DecoderOptions GeneralOptions { get; init; } = new();
/// <summary>
/// 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.InteropServices;
using System.Runtime.Intrinsics;
using System.Text;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Png.Chunks;
@ -217,6 +218,7 @@ internal sealed class PngEncoderCore : IDisposable
this.WritePhysicalChunk(stream, metadata);
this.WriteExifChunk(stream, metadata);
this.WriteXmpChunk(stream, metadata);
this.WriteIptcChunk(stream, metadata);
this.WriteTextChunks(stream, pngMetadata);
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
// quantize the subsequent frames. This allows us to cache the color matching resolution.
paletteQuantizer ??= new(
paletteQuantizer ??= new PaletteQuantizer<TPixel>(
this.configuration,
this.quantizer!.Options,
previousPalette);
@ -889,6 +891,163 @@ internal sealed class PngEncoderCore : IDisposable
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>
/// Writes the CICP profile chunk
/// </summary>

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

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

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

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.PixelFormats;
@ -31,7 +32,7 @@ public class PngFrameMetadata : IFormatFrameMetadata<PngFrameMetadata>
/// <summary>
/// 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.
/// The clock starts ticking immediately after the graphic is rendered.
/// </summary>
@ -62,7 +63,7 @@ public class PngFrameMetadata : IFormatFrameMetadata<PngFrameMetadata>
public static PngFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata)
=> new()
{
FrameDelay = new(metadata.Duration.TotalMilliseconds / 1000),
FrameDelay = new Rational(metadata.Duration.TotalMilliseconds / 1000),
DisposalMode = GetMode(metadata.DisposalMode),
BlendMode = metadata.BlendMode,
};
@ -76,7 +77,7 @@ public class PngFrameMetadata : IFormatFrameMetadata<PngFrameMetadata>
delay = 0;
}
return new()
return new FormatConnectingFrameMetadata
{
ColorTableMode = FrameColorTableMode.Global,
Duration = TimeSpan.FromMilliseconds(delay * 1000),
@ -86,7 +87,7 @@ public class PngFrameMetadata : IFormatFrameMetadata<PngFrameMetadata>
}
/// <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>
{
}

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

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.PixelFormats;
@ -110,7 +111,7 @@ public class PngMetadata : IFormatMetadata<PngMetadata>
color = PngColorType.Rgb;
break;
default:
if (colorType.HasFlag(PixelColorType.Luminance))
if (colorType.HasFlag(PixelColorType.Luminance | PixelColorType.Alpha))
{
color = PngColorType.GrayscaleWithAlpha;
break;
@ -129,7 +130,7 @@ public class PngMetadata : IFormatMetadata<PngMetadata>
4 => PngBitDepth.Bit4,
_ => (bpc <= 8) ? PngBitDepth.Bit8 : PngBitDepth.Bit16,
};
return new()
return new PngMetadata
{
ColorType = color,
BitDepth = bitDepth,
@ -227,9 +228,26 @@ public class PngMetadata : IFormatMetadata<PngMetadata>
};
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination)
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
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/>
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 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
@ -143,7 +143,7 @@ internal static class PngScanlineProcessor
{
byte l = Unsafe.Add(ref scanlineSpanRef, offset2);
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;
}
}
@ -239,7 +239,7 @@ internal static class PngScanlineProcessor
ushort r = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
ushort g = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 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)
@ -258,7 +258,7 @@ internal static class PngScanlineProcessor
byte r = Unsafe.Add(ref scanlineSpanRef, (uint)o);
byte g = Unsafe.Add(ref scanlineSpanRef, (uint)(o + 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 b = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * 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)
@ -360,7 +360,7 @@ internal static class PngScanlineProcessor
byte g = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample));
byte b = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * 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.
/// See https://github.com/phoboslab/qoi/issues/167
/// </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>
/// Gets the list of file extensions that equate to a QOI.
/// </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>
/// Gets the shared instance.
/// </summary>
public static QoiFormat Instance { get; } = new QoiFormat();
public static QoiFormat Instance { get; } = new();
/// <inheritdoc/>
public string DefaultMimeType => "image/qoi";

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

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

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

@ -8,12 +8,12 @@ internal static class TgaConstants
/// <summary>
/// The list of mimetypes that equate to a targa file.
/// </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>
/// The list of file extensions that equate to a targa file.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "tga", "vda", "icb", "vst" };
public static readonly IEnumerable<string> FileExtensions = ["tga", "vda", "icb", "vst"];
/// <summary>
/// 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