Browse Source

Merge branch 'main' into sn/fix_rider-errors

pull/3013/head
Stefan Nikolei 6 months ago
committed by GitHub
parent
commit
ccf2baa038
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 51
      .github/workflows/build-and-test.yml
  2. 6
      Directory.Build.props
  3. 19
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  4. 21
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  5. 236
      src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
  6. 187
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
  7. 28
      src/ImageSharp/Image.FromBytes.cs
  8. 5
      src/ImageSharp/Image.LoadPixelData.cs
  9. 2
      src/ImageSharp/ImageSharp.csproj
  10. 14
      src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
  11. 12
      src/ImageSharp/Primitives/Point.cs
  12. 12
      src/ImageSharp/Primitives/PointF.cs
  13. 12
      src/ImageSharp/Primitives/SizeF.cs
  14. 43
      src/ImageSharp/Processing/AffineTransformBuilder.cs
  15. 8
      src/ImageSharp/Processing/Extensions/Transforms/TransformExtensions.cs
  16. 2
      src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor.cs
  17. 4
      src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs
  18. 2
      src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor.cs
  19. 8
      src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs
  20. 4
      src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs
  21. 4
      src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs
  22. 2
      src/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler,TPixel}.cs
  23. 376
      src/ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs
  24. 66
      src/ImageSharp/Processing/ProjectiveTransformBuilder.cs
  25. 26
      src/ImageSharp/Processing/TransformSpace.cs
  26. 2
      tests/Directory.Build.targets
  27. 4
      tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj
  28. 2
      tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj
  29. 10
      tests/ImageSharp.Tests/Common/SimdUtilsTests.Shuffle.cs
  30. 4
      tests/ImageSharp.Tests/Common/SimdUtilsTests.cs
  31. 4
      tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs
  32. 13
      tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs
  33. 14
      tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs
  34. 16
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  35. 2
      tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs
  36. 2
      tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.cs
  37. 2
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  38. 6
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  39. 4
      tests/ImageSharp.Tests/Formats/WebP/ColorSpaceTransformUtilsTests.cs
  40. 14
      tests/ImageSharp.Tests/Formats/WebP/LosslessUtilsTests.cs
  41. 4
      tests/ImageSharp.Tests/Formats/WebP/PredictorEncoderTests.cs
  42. 2
      tests/ImageSharp.Tests/Formats/WebP/QuantEncTests.cs
  43. 2
      tests/ImageSharp.Tests/Formats/WebP/Vp8ResidualTests.cs
  44. 4
      tests/ImageSharp.Tests/Formats/WebP/WebpCommonUtilsTests.cs
  45. 2
      tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs
  46. 4
      tests/ImageSharp.Tests/Image/ImageTests.DetectFormat.cs
  47. 4
      tests/ImageSharp.Tests/Image/ImageTests.Identify.cs
  48. 11
      tests/ImageSharp.Tests/Image/ImageTests.Load_FromBytes_PassLocalConfiguration.cs
  49. 4
      tests/ImageSharp.Tests/Image/ImageTests.Load_FromBytes_UseGlobalConfiguration.cs
  50. 13
      tests/ImageSharp.Tests/Image/ImageTests.Load_FromStream_ThrowsRightException.cs
  51. 2
      tests/ImageSharp.Tests/ImageSharp.Tests.csproj
  52. 4
      tests/ImageSharp.Tests/Metadata/Profiles/ICC/DataWriter/IccDataWriterPrimitivesTests.cs
  53. 2
      tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs
  54. 40
      tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs
  55. 8
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ProjectiveTransformTests.cs
  56. 2
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs
  57. 8
      tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs
  58. 2
      tests/ImageSharp.Tests/Processing/Transforms/ResizeTests.cs
  59. 4
      tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs
  60. 4
      tests/ImageSharp.Tests/TestImages.cs
  61. 65
      tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs
  62. 259
      tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs
  63. 3
      tests/Images/External/ReferenceOutput/AffineTransformTests/Issue3000_Rgba32_issue_3000_p-3-3.png
  64. 3
      tests/Images/External/ReferenceOutput/AffineTransformTests/Issue3000_Rgba32_issue_3000_p-4-4.png
  65. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_FromSourceRectangle1_Rgba32_TestPattern96x48.png
  66. 3
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_FromSourceRectangle1_Rgba32_TestPattern96x48__original.png
  67. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_FromSourceRectangle2_Rgba32_TestPattern96x48.png
  68. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(0)_S(1,2)_T(0,0).png
  69. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(0)_S(2,1)_T(0,0).png
  70. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(-20,-10).png
  71. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(0,0).png
  72. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(20,10).png
  73. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.1,1.3)_T(30,-20).png
  74. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.5,1.5)_T(0,0).png
  75. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScale_ManuallyCentered_Rgba32_TestPattern96x96_R(50)_S(0.8).png
  76. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png
  77. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png
  78. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png
  79. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png
  80. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png
  81. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png
  82. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png
  83. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png
  84. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png
  85. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png
  86. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png
  87. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png
  88. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png
  89. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png
  90. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png
  91. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png
  92. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png
  93. 4
      tests/Images/External/ReferenceOutput/Drawing/DrawImageTests/DrawTransformed.png
  94. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=-50, Y=-50 ]-PointF [ X=200, Y=-50 ]-PointF [ X=200, Y=200 ]-PointF [ X=-50, Y=200 ].png
  95. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=0, Y=0 ]-PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ].png
  96. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ]-PointF [ X=0, Y=0 ].png
  97. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=25, Y=50 ]-PointF [ X=210, Y=25 ]-PointF [ X=140, Y=210 ]-PointF [ X=15, Y=125 ].png
  98. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png
  99. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png
  100. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png

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

@ -62,30 +62,34 @@ jobs:
needs: WarmLFS needs: WarmLFS
strategy: strategy:
matrix: matrix:
isARM:
- ${{ contains(github.event.pull_request.labels.*.name, 'arch:arm32') || contains(github.event.pull_request.labels.*.name, 'arch:arm64') }}
options: options:
- os: ubuntu-latest - os: ubuntu-latest
framework: net9.0 framework: net10.0
sdk: 9.0.x sdk: 10.0.x
sdk-preview: true sdk-preview: true
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable
framework: net9.0 framework: net10.0
sdk: 9.0.x sdk: 10.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: macos-26
framework: net10.0
sdk: 10.0.x
sdk-preview: true sdk-preview: true
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: windows-latest - os: windows-latest
framework: net9.0 framework: net10.0
sdk: 9.0.x sdk: 10.0.x
sdk-preview: true sdk-preview: true
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: buildjet-4vcpu-ubuntu-2204-arm - os: ubuntu-22.04-arm
framework: net9.0 framework: net10.0
sdk: 9.0.x sdk: 10.0.x
sdk-preview: true sdk-preview: true
runtime: -x64 runtime: -x64
codecov: false codecov: false
@ -100,20 +104,21 @@ jobs:
sdk: 8.0.x sdk: 8.0.x
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: macos-26
framework: net8.0
sdk: 8.0.x
runtime: -x64
codecov: false
- os: windows-latest - os: windows-latest
framework: net8.0 framework: net8.0
sdk: 8.0.x sdk: 8.0.x
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: buildjet-4vcpu-ubuntu-2204-arm - os: ubuntu-22.04-arm
framework: net8.0 framework: net8.0
sdk: 8.0.x sdk: 8.0.x
runtime: -x64 runtime: -x64
codecov: false codecov: false
exclude:
- isARM: false
options:
os: buildjet-4vcpu-ubuntu-2204-arm
runs-on: ${{ matrix.options.os }} runs-on: ${{ matrix.options.os }}
@ -124,6 +129,18 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev
- name: Install libgdi+, which is required for tests running on macos
if: ${{ contains(matrix.options.os, 'macos-26') }}
run: |
brew update
brew install mono-libgdiplus
# Create symlinks to make libgdiplus discoverable
sudo mkdir -p /usr/local/lib
sudo ln -sf $(brew --prefix)/lib/libgdiplus.dylib /usr/local/lib/libgdiplus.dylib
# Verify installation
ls -la $(brew --prefix)/lib/libgdiplus* || echo "libgdiplus not found in brew prefix"
ls -la /usr/local/lib/libgdiplus* || echo "libgdiplus not found in /usr/local/lib"
- name: Git Config - name: Git Config
shell: bash shell: bash
run: | run: |
@ -170,7 +187,7 @@ jobs:
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: | dotnet-version: |
9.0.x 10.0.x
- name: DotNet Build - name: DotNet Build
if: ${{ matrix.options.sdk-preview != true }} if: ${{ matrix.options.sdk-preview != true }}

6
Directory.Build.props

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

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

@ -71,6 +71,11 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
/// </summary> /// </summary>
private bool hasAdobeMarker; private bool hasAdobeMarker;
/// <summary>
/// Whether the image has a SOS marker.
/// </summary>
private bool hasSOSMarker;
/// <summary> /// <summary>
/// Contains information about the JFIF marker. /// Contains information about the JFIF marker.
/// </summary> /// </summary>
@ -197,6 +202,12 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
{ {
using SpectralConverter<TPixel> spectralConverter = new(this.configuration, this.resizeMode == JpegDecoderResizeMode.ScaleOnly ? null : this.Options.TargetSize); using SpectralConverter<TPixel> spectralConverter = new(this.configuration, this.resizeMode == JpegDecoderResizeMode.ScaleOnly ? null : this.Options.TargetSize);
this.ParseStream(stream, spectralConverter, cancellationToken); this.ParseStream(stream, spectralConverter, cancellationToken);
if (!this.hasSOSMarker)
{
JpegThrowHelper.ThrowInvalidImageContentException("Missing SOS marker.");
}
this.InitExifProfile(); this.InitExifProfile();
this.InitIccProfile(); this.InitIccProfile();
this.InitIptcProfile(); this.InitIptcProfile();
@ -215,6 +226,12 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{ {
this.ParseStream(stream, spectralConverter: null, cancellationToken); this.ParseStream(stream, spectralConverter: null, cancellationToken);
if (!this.hasSOSMarker)
{
JpegThrowHelper.ThrowInvalidImageContentException("Missing SOS marker.");
}
this.InitExifProfile(); this.InitExifProfile();
this.InitIccProfile(); this.InitIccProfile();
this.InitIptcProfile(); this.InitIptcProfile();
@ -403,6 +420,8 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
break; break;
case JpegConstants.Markers.SOS: case JpegConstants.Markers.SOS:
this.hasSOSMarker = true;
if (!metadataOnly) if (!metadataOnly)
{ {
this.ProcessStartOfScanMarker(stream, markerContentByteSize); this.ProcessStartOfScanMarker(stream, markerContentByteSize);

21
src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs

@ -112,12 +112,12 @@ internal class WebpAnimationDecoder : IDisposable
this.webpMetadata = this.metadata.GetWebpMetadata(); this.webpMetadata = this.metadata.GetWebpMetadata();
this.webpMetadata.RepeatCount = features.AnimationLoopCount; this.webpMetadata.RepeatCount = features.AnimationLoopCount;
Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore this.webpMetadata.BackgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore
? Color.Transparent ? Color.Transparent
: features.AnimationBackgroundColor!.Value; : features.AnimationBackgroundColor!.Value;
this.webpMetadata.BackgroundColor = backgroundColor; bool ignoreMetadata = this.skipMetadata;
SegmentIntegrityHandling segmentIntegrityHandling = this.segmentIntegrityHandling;
Span<byte> buffer = stackalloc byte[4]; Span<byte> buffer = stackalloc byte[4];
uint frameCount = 0; uint frameCount = 0;
int remainingBytes = (int)completeDataSize; int remainingBytes = (int)completeDataSize;
@ -135,9 +135,16 @@ internal class WebpAnimationDecoder : IDisposable
remainingBytes -= (int)dataSize; remainingBytes -= (int)dataSize;
break; break;
case WebpChunkType.Iccp:
case WebpChunkType.Xmp: case WebpChunkType.Xmp:
case WebpChunkType.Exif: case WebpChunkType.Exif:
WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, this.metadata, this.skipMetadata, this.segmentIntegrityHandling, buffer); WebpChunkParsingUtils.ParseOptionalChunks(
stream,
chunkType,
this.metadata,
ignoreMetadata,
segmentIntegrityHandling,
buffer);
break; break;
default: default:
@ -187,9 +194,12 @@ internal class WebpAnimationDecoder : IDisposable
this.webpMetadata.BackgroundColor = backgroundColor; this.webpMetadata.BackgroundColor = backgroundColor;
TPixel backgroundPixel = backgroundColor.ToPixel<TPixel>(); TPixel backgroundPixel = backgroundColor.ToPixel<TPixel>();
bool ignoreMetadata = this.skipMetadata;
SegmentIntegrityHandling segmentIntegrityHandling = this.segmentIntegrityHandling;
Span<byte> buffer = stackalloc byte[4]; Span<byte> buffer = stackalloc byte[4];
uint frameCount = 0; uint frameCount = 0;
int remainingBytes = (int)completeDataSize; int remainingBytes = (int)completeDataSize;
while (remainingBytes > 0) while (remainingBytes > 0)
{ {
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer);
@ -209,9 +219,10 @@ internal class WebpAnimationDecoder : IDisposable
remainingBytes -= (int)dataSize; remainingBytes -= (int)dataSize;
break; break;
case WebpChunkType.Iccp:
case WebpChunkType.Xmp: case WebpChunkType.Xmp:
case WebpChunkType.Exif: case WebpChunkType.Exif:
WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, this.skipMetadata, this.segmentIntegrityHandling, buffer); WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, ignoreMetadata, segmentIntegrityHandling, buffer);
break; break;
default: default:

236
src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs

@ -9,6 +9,7 @@ using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp; namespace SixLabors.ImageSharp.Formats.Webp;
@ -258,6 +259,9 @@ internal static class WebpChunkParsingUtils
/// <param name="stream">The stream to read from.</param> /// <param name="stream">The stream to read from.</param>
/// <param name="buffer">The buffer to store the read data into.</param> /// <param name="buffer">The buffer to store the read data into.</param>
/// <returns>A unsigned 24 bit integer.</returns> /// <returns>A unsigned 24 bit integer.</returns>
/// <exception cref="ImageFormatException">
/// Thrown if the input stream is not valid.
/// </exception>
public static uint ReadUInt24LittleEndian(Stream stream, Span<byte> buffer) public static uint ReadUInt24LittleEndian(Stream stream, Span<byte> buffer)
{ {
if (stream.Read(buffer, 0, 3) == 3) if (stream.Read(buffer, 0, 3) == 3)
@ -272,8 +276,11 @@ internal static class WebpChunkParsingUtils
/// <summary> /// <summary>
/// Writes a unsigned 24 bit integer. /// Writes a unsigned 24 bit integer.
/// </summary> /// </summary>
/// <param name="stream">The stream to read from.</param> /// <param name="stream">The stream to write to.</param>
/// <param name="data">The uint24 data to write.</param> /// <param name="data">The uint24 data to write.</param>
/// <exception cref="InvalidDataException">
/// Thrown if the data is not a valid unsigned 24 bit integer.
/// </exception>
public static unsafe void WriteUInt24LittleEndian(Stream stream, uint data) public static unsafe void WriteUInt24LittleEndian(Stream stream, uint data)
{ {
if (data >= 1 << 24) if (data >= 1 << 24)
@ -296,18 +303,24 @@ internal static class WebpChunkParsingUtils
/// </summary> /// </summary>
/// <param name="stream">The stream to read the data from.</param> /// <param name="stream">The stream to read the data from.</param>
/// <param name="buffer">Buffer to store the data read from the stream.</param> /// <param name="buffer">Buffer to store the data read from the stream.</param>
/// <param name="required">If true, the chunk size is required to be read, otherwise it can be skipped.</param>
/// <returns>The chunk size in bytes.</returns> /// <returns>The chunk size in bytes.</returns>
public static uint ReadChunkSize(Stream stream, Span<byte> buffer) /// <exception cref="ImageFormatException">Thrown if the input stream is not valid.</exception>
public static uint ReadChunkSize(Stream stream, Span<byte> buffer, bool required = true)
{ {
DebugGuard.IsTrue(buffer.Length is 4, "buffer has wrong length");
if (stream.Read(buffer) is 4) if (stream.Read(buffer) is 4)
{ {
uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return chunkSize % 2 is 0 ? chunkSize : chunkSize + 1; return chunkSize % 2 is 0 ? chunkSize : chunkSize + 1;
} }
throw new ImageFormatException("Invalid Webp data, could not read chunk size."); if (required)
{
throw new ImageFormatException("Invalid Webp data, could not read chunk size.");
}
// Return the size of the remaining data in the stream.
return (uint)(stream.Length - stream.Position);
} }
/// <summary> /// <summary>
@ -320,14 +333,13 @@ internal static class WebpChunkParsingUtils
/// </exception> /// </exception>
public static WebpChunkType ReadChunkType(BufferedReadStream stream, Span<byte> buffer) public static WebpChunkType ReadChunkType(BufferedReadStream stream, Span<byte> buffer)
{ {
DebugGuard.IsTrue(buffer.Length == 4, "buffer has wrong length");
if (stream.Read(buffer) == 4) if (stream.Read(buffer) == 4)
{ {
WebpChunkType chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer); return (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer);
return chunkType;
} }
// While we ignore unknown chunks we still need a to be a ble to read a chunk type
// known or otherwise from the stream.
throw new ImageFormatException("Invalid Webp data, could not read chunk type."); throw new ImageFormatException("Invalid Webp data, could not read chunk type.");
} }
@ -336,82 +348,182 @@ internal static class WebpChunkParsingUtils
/// If there are more such chunks, readers MAY ignore all except the first one. /// If there are more such chunks, readers MAY ignore all except the first one.
/// Also, a file may possibly contain both 'EXIF' and 'XMP ' chunks. /// Also, a file may possibly contain both 'EXIF' and 'XMP ' chunks.
/// </summary> /// </summary>
/// <param name="stream">The stream to read the data from.</param>
/// <param name="chunkType">The chunk type to parse.</param>
/// <param name="metadata">The image metadata to write to.</param>
/// <param name="ignoreMetadata">If true, metadata will be ignored.</param>
/// <param name="segmentIntegrityHandling">Indicates how to handle segment integrity issues.</param>
/// <param name="buffer">Buffer to store the data read from the stream.</param>
public static void ParseOptionalChunks( public static void ParseOptionalChunks(
BufferedReadStream stream, BufferedReadStream stream,
WebpChunkType chunkType, WebpChunkType chunkType,
ImageMetadata metadata, ImageMetadata metadata,
bool ignoreMetaData, bool ignoreMetadata,
SegmentIntegrityHandling segmentIntegrityHandling, SegmentIntegrityHandling segmentIntegrityHandling,
Span<byte> buffer) Span<byte> buffer)
{ {
long streamLength = stream.Length; long streamLength = stream.Length;
while (stream.Position < streamLength) while (stream.Position < streamLength)
{ {
uint chunkLength = ReadChunkSize(stream, buffer);
if (ignoreMetaData)
{
stream.Skip((int)chunkLength);
}
int bytesRead;
switch (chunkType) switch (chunkType)
{ {
case WebpChunkType.Exif: case WebpChunkType.Iccp:
byte[] exifData = new byte[chunkLength]; ReadIccProfile(stream, metadata, ignoreMetadata, segmentIntegrityHandling, buffer);
bytesRead = stream.Read(exifData, 0, (int)chunkLength); break;
if (bytesRead != chunkLength)
{
if (segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone)
{
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile");
}
return;
}
if (metadata.ExifProfile == null)
{
ExifProfile exifProfile = new(exifData);
// Set the resolution from the metadata.
double horizontalValue = GetExifResolutionValue(exifProfile, ExifTag.XResolution);
double verticalValue = GetExifResolutionValue(exifProfile, ExifTag.YResolution);
if (horizontalValue > 0 && verticalValue > 0)
{
metadata.HorizontalResolution = horizontalValue;
metadata.VerticalResolution = verticalValue;
metadata.ResolutionUnits = UnitConverter.ExifProfileToResolutionUnit(exifProfile);
}
metadata.ExifProfile = exifProfile;
}
case WebpChunkType.Exif:
ReadExifProfile(stream, metadata, ignoreMetadata, segmentIntegrityHandling, buffer);
break; break;
case WebpChunkType.Xmp: case WebpChunkType.Xmp:
byte[] xmpData = new byte[chunkLength]; ReadXmpProfile(stream, metadata, ignoreMetadata, segmentIntegrityHandling, buffer);
bytesRead = stream.Read(xmpData, 0, (int)chunkLength);
if (bytesRead != chunkLength)
{
if (segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone)
{
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile");
}
return;
}
metadata.XmpProfile ??= new XmpProfile(xmpData);
break; break;
default: default:
// Ignore unknown chunks.
// These must always fall after the image data so we are safe to always skip them.
uint chunkLength = ReadChunkSize(stream, buffer, false);
stream.Skip((int)chunkLength); stream.Skip((int)chunkLength);
break; break;
} }
} }
} }
/// <summary>
/// Reads the ICCP chunk from the stream.
/// </summary>
/// <param name="stream">The stream to decode from.</param>
/// <param name="metadata">The image metadata.</param>
/// <param name="ignoreMetadata">If true, metadata will be ignored.</param>
/// <param name="segmentIntegrityHandling">Indicates how to handle segment integrity issues.</param>
/// <param name="buffer">Temporary buffer.</param>
public static void ReadIccProfile(
BufferedReadStream stream,
ImageMetadata metadata,
bool ignoreMetadata,
SegmentIntegrityHandling segmentIntegrityHandling,
Span<byte> buffer)
{
// While ICC profiles are optional, an invalid ICC profile cannot be ignored as it must precede the image data
// and since we canot determine its size to allow skipping without reading the chunk size, we have to throw if it's invalid.
// Hence we do not consider segment integrity handling here.
uint iccpChunkSize = ReadChunkSize(stream, buffer);
if (ignoreMetadata || metadata.IccProfile != null)
{
stream.Skip((int)iccpChunkSize);
}
else
{
bool ignoreNone = segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone;
byte[] iccpData = new byte[iccpChunkSize];
int bytesRead = stream.Read(iccpData, 0, (int)iccpChunkSize);
// We have the size but the profile is invalid if we cannot read enough data.
// Use the segment integrity handling to determine if we throw.
if (bytesRead != iccpChunkSize && ignoreNone)
{
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk");
}
IccProfile profile = new(iccpData);
if (profile.CheckIsValid())
{
metadata.IccProfile = profile;
}
}
}
/// <summary>
/// Reads the EXIF profile from the stream.
/// </summary>
/// <param name="stream">The stream to decode from.</param>
/// <param name="metadata">The image metadata.</param>
/// <param name="ignoreMetadata">If true, metadata will be ignored.</param>
/// <param name="segmentIntegrityHandling">Indicates how to handle segment integrity issues.</param>
/// <param name="buffer">Temporary buffer.</param>
public static void ReadExifProfile(
BufferedReadStream stream,
ImageMetadata metadata,
bool ignoreMetadata,
SegmentIntegrityHandling segmentIntegrityHandling,
Span<byte> buffer)
{
bool ignoreNone = segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone;
uint exifChunkSize = ReadChunkSize(stream, buffer, ignoreNone);
if (ignoreMetadata || metadata.ExifProfile != null)
{
stream.Skip((int)exifChunkSize);
}
else
{
byte[] exifData = new byte[exifChunkSize];
int bytesRead = stream.Read(exifData, 0, (int)exifChunkSize);
if (bytesRead != exifChunkSize)
{
if (ignoreNone)
{
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile");
}
return;
}
ExifProfile exifProfile = new(exifData);
// Set the resolution from the metadata.
double horizontalValue = GetExifResolutionValue(exifProfile, ExifTag.XResolution);
double verticalValue = GetExifResolutionValue(exifProfile, ExifTag.YResolution);
if (horizontalValue > 0 && verticalValue > 0)
{
metadata.HorizontalResolution = horizontalValue;
metadata.VerticalResolution = verticalValue;
metadata.ResolutionUnits = UnitConverter.ExifProfileToResolutionUnit(exifProfile);
}
metadata.ExifProfile = exifProfile;
}
}
/// <summary>
/// Reads the XMP profile the stream.
/// </summary>
/// <param name="stream">The stream to decode from.</param>
/// <param name="metadata">The image metadata.</param>
/// <param name="ignoreMetadata">If true, metadata will be ignored.</param>
/// <param name="segmentIntegrityHandling">Indicates how to handle segment integrity issues.</param>
/// <param name="buffer">Temporary buffer.</param>
public static void ReadXmpProfile(
BufferedReadStream stream,
ImageMetadata metadata,
bool ignoreMetadata,
SegmentIntegrityHandling segmentIntegrityHandling,
Span<byte> buffer)
{
bool ignoreNone = segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone;
uint xmpChunkSize = ReadChunkSize(stream, buffer, ignoreNone);
if (ignoreMetadata || metadata.XmpProfile != null)
{
stream.Skip((int)xmpChunkSize);
}
else
{
byte[] xmpData = new byte[xmpChunkSize];
int bytesRead = stream.Read(xmpData, 0, (int)xmpChunkSize);
if (bytesRead != xmpChunkSize)
{
if (ignoreNone)
{
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile");
}
return;
}
metadata.XmpProfile = new XmpProfile(xmpData);
}
}
private static double GetExifResolutionValue(ExifProfile exifProfile, ExifTag<Rational> tag) private static double GetExifResolutionValue(ExifProfile exifProfile, ExifTag<Rational> tag)
{ {
if (exifProfile.TryGetValue(tag, out IExifValue<Rational>? resolution)) if (exifProfile.TryGetValue(tag, out IExifValue<Rational>? resolution))

187
src/ImageSharp/Formats/Webp/WebpDecoderCore.cs

@ -3,15 +3,11 @@
using System.Buffers; using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp; namespace SixLabors.ImageSharp.Formats.Webp;
@ -248,7 +244,8 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
else else
{ {
// Ignore unknown chunks. // Ignore unknown chunks.
uint chunkSize = ReadChunkSize(stream, buffer, false); // These must always fall after the image data so we are safe to always skip them.
uint chunkSize = WebpChunkParsingUtils.ReadChunkSize(stream, buffer, false);
stream.Skip((int)chunkSize); stream.Skip((int)chunkSize);
} }
} }
@ -279,18 +276,20 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
bool ignoreAlpha, bool ignoreAlpha,
Span<byte> buffer) Span<byte> buffer)
{ {
bool ignoreMetadata = this.skipMetadata;
SegmentIntegrityHandling integrityHandling = this.segmentIntegrityHandling;
switch (chunkType) switch (chunkType)
{ {
case WebpChunkType.Iccp: case WebpChunkType.Iccp:
this.ReadIccProfile(stream, metadata, buffer); WebpChunkParsingUtils.ReadIccProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer);
break; break;
case WebpChunkType.Exif: case WebpChunkType.Exif:
this.ReadExifProfile(stream, metadata, buffer); WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer);
break; break;
case WebpChunkType.Xmp: case WebpChunkType.Xmp:
this.ReadXmpProfile(stream, metadata, buffer); WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer);
break; break;
case WebpChunkType.AnimationParameter: case WebpChunkType.AnimationParameter:
@ -319,7 +318,10 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
/// <param name="buffer">Temporary buffer.</param> /// <param name="buffer">Temporary buffer.</param>
private void ParseOptionalChunks(BufferedReadStream stream, ImageMetadata metadata, WebpFeatures features, Span<byte> buffer) private void ParseOptionalChunks(BufferedReadStream stream, ImageMetadata metadata, WebpFeatures features, Span<byte> buffer)
{ {
if (this.skipMetadata || (!features.ExifProfile && !features.XmpMetaData)) bool ignoreMetadata = this.skipMetadata;
SegmentIntegrityHandling integrityHandling = this.segmentIntegrityHandling;
if (ignoreMetadata || (!features.ExifProfile && !features.XmpMetaData))
{ {
return; return;
} }
@ -328,139 +330,24 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
while (stream.Position < streamLength) while (stream.Position < streamLength)
{ {
// Read chunk header. // Read chunk header.
WebpChunkType chunkType = ReadChunkType(stream, buffer); WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer);
if (chunkType == WebpChunkType.Exif && metadata.ExifProfile == null) if (chunkType == WebpChunkType.Exif && metadata.ExifProfile == null)
{ {
this.ReadExifProfile(stream, metadata, buffer); WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer);
} }
else if (chunkType == WebpChunkType.Xmp && metadata.XmpProfile == null) else if (chunkType == WebpChunkType.Xmp && metadata.XmpProfile == null)
{ {
this.ReadXmpProfile(stream, metadata, buffer); WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer);
} }
else else
{ {
// Skip duplicate XMP or EXIF chunk. // Skip duplicate XMP or EXIF chunk.
uint chunkLength = ReadChunkSize(stream, buffer); uint chunkLength = WebpChunkParsingUtils.ReadChunkSize(stream, buffer, false);
stream.Skip((int)chunkLength); stream.Skip((int)chunkLength);
} }
} }
} }
/// <summary>
/// Reads the EXIF profile from the stream.
/// </summary>
/// <param name="stream">The stream to decode from.</param>
/// <param name="metadata">The image metadata.</param>
/// <param name="buffer">Temporary buffer.</param>
private void ReadExifProfile(BufferedReadStream stream, ImageMetadata metadata, Span<byte> buffer)
{
uint exifChunkSize = ReadChunkSize(stream, buffer);
if (this.skipMetadata)
{
stream.Skip((int)exifChunkSize);
}
else
{
byte[] exifData = new byte[exifChunkSize];
int bytesRead = stream.Read(exifData, 0, (int)exifChunkSize);
if (bytesRead != exifChunkSize)
{
if (this.segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone)
{
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the EXIF profile");
}
return;
}
ExifProfile exifProfile = new(exifData);
// Set the resolution from the metadata.
double horizontalValue = GetExifResolutionValue(exifProfile, ExifTag.XResolution);
double verticalValue = GetExifResolutionValue(exifProfile, ExifTag.YResolution);
if (horizontalValue > 0 && verticalValue > 0)
{
metadata.HorizontalResolution = horizontalValue;
metadata.VerticalResolution = verticalValue;
metadata.ResolutionUnits = UnitConverter.ExifProfileToResolutionUnit(exifProfile);
}
metadata.ExifProfile = exifProfile;
}
}
private static double GetExifResolutionValue(ExifProfile exifProfile, ExifTag<Rational> tag)
{
if (exifProfile.TryGetValue(tag, out IExifValue<Rational>? resolution))
{
return resolution.Value.ToDouble();
}
return 0;
}
/// <summary>
/// Reads the XMP profile the stream.
/// </summary>
/// <param name="stream">The stream to decode from.</param>
/// <param name="metadata">The image metadata.</param>
/// <param name="buffer">Temporary buffer.</param>
private void ReadXmpProfile(BufferedReadStream stream, ImageMetadata metadata, Span<byte> buffer)
{
uint xmpChunkSize = ReadChunkSize(stream, buffer);
if (this.skipMetadata)
{
stream.Skip((int)xmpChunkSize);
}
else
{
byte[] xmpData = new byte[xmpChunkSize];
int bytesRead = stream.Read(xmpData, 0, (int)xmpChunkSize);
if (bytesRead != xmpChunkSize)
{
if (this.segmentIntegrityHandling == SegmentIntegrityHandling.IgnoreNone)
{
WebpThrowHelper.ThrowImageFormatException("Could not read enough data for the XMP profile");
}
return;
}
metadata.XmpProfile = new XmpProfile(xmpData);
}
}
/// <summary>
/// Reads the ICCP chunk from the stream.
/// </summary>
/// <param name="stream">The stream to decode from.</param>
/// <param name="metadata">The image metadata.</param>
/// <param name="buffer">Temporary buffer.</param>
private void ReadIccProfile(BufferedReadStream stream, ImageMetadata metadata, Span<byte> buffer)
{
uint iccpChunkSize = ReadChunkSize(stream, buffer);
if (this.skipMetadata)
{
stream.Skip((int)iccpChunkSize);
}
else
{
byte[] iccpData = new byte[iccpChunkSize];
int bytesRead = stream.Read(iccpData, 0, (int)iccpChunkSize);
if (bytesRead != iccpChunkSize)
{
WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk");
}
IccProfile profile = new(iccpData);
if (profile.CheckIsValid())
{
metadata.IccProfile = profile;
}
}
}
/// <summary> /// <summary>
/// Reads the animation parameters chunk from the stream. /// Reads the animation parameters chunk from the stream.
/// </summary> /// </summary>
@ -512,50 +399,6 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
} }
} }
/// <summary>
/// Identifies the chunk type from the chunk.
/// </summary>
/// <param name="stream">The stream to decode from.</param>
/// <param name="buffer">Temporary buffer.</param>
/// <exception cref="ImageFormatException">
/// Thrown if the input stream is not valid.
/// </exception>
private static WebpChunkType ReadChunkType(BufferedReadStream stream, Span<byte> buffer)
{
if (stream.Read(buffer, 0, 4) == 4)
{
return (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer);
}
throw new ImageFormatException("Invalid Webp data.");
}
/// <summary>
/// Reads the chunk size. If Chunk Size is odd, a single padding byte will be added to the payload,
/// so the chunk size will be increased by 1 in those cases.
/// </summary>
/// <param name="stream">The stream to decode from.</param>
/// <param name="buffer">Temporary buffer.</param>
/// <param name="required">If true, the chunk size is required to be read, otherwise it can be skipped.</param>
/// <returns>The chunk size in bytes.</returns>
/// <exception cref="ImageFormatException">Invalid data.</exception>
private static uint ReadChunkSize(BufferedReadStream stream, Span<byte> buffer, bool required = true)
{
if (stream.Read(buffer, 0, 4) == 4)
{
uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return (chunkSize % 2 == 0) ? chunkSize : chunkSize + 1;
}
if (required)
{
throw new ImageFormatException("Invalid Webp data.");
}
// Return the size of the remaining data in the stream.
return (uint)(stream.Length - stream.Position);
}
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() => this.alphaData?.Dispose(); public void Dispose() => this.alphaData?.Dispose();
} }

28
src/ImageSharp/Image.FromBytes.cs

@ -34,7 +34,12 @@ public abstract partial class Image
/// <exception cref="UnknownImageFormatException">The encoded image format is unknown.</exception> /// <exception cref="UnknownImageFormatException">The encoded image format is unknown.</exception>
public static unsafe IImageFormat DetectFormat(DecoderOptions options, ReadOnlySpan<byte> buffer) public static unsafe IImageFormat DetectFormat(DecoderOptions options, ReadOnlySpan<byte> buffer)
{ {
Guard.NotNull(options, nameof(options.Configuration)); Guard.NotNull(options, nameof(options));
if (buffer.IsEmpty)
{
throw new UnknownImageFormatException("Cannot detect image format from empty data.");
}
fixed (byte* ptr = buffer) fixed (byte* ptr = buffer)
{ {
@ -66,6 +71,13 @@ public abstract partial class Image
/// <exception cref="UnknownImageFormatException">The encoded image format is unknown.</exception> /// <exception cref="UnknownImageFormatException">The encoded image format is unknown.</exception>
public static unsafe ImageInfo Identify(DecoderOptions options, ReadOnlySpan<byte> buffer) public static unsafe ImageInfo Identify(DecoderOptions options, ReadOnlySpan<byte> buffer)
{ {
Guard.NotNull(options, nameof(options));
if (buffer.IsEmpty)
{
throw new UnknownImageFormatException("Cannot identify image format from empty data.");
}
fixed (byte* ptr = buffer) fixed (byte* ptr = buffer)
{ {
using UnmanagedMemoryStream stream = new(ptr, buffer.Length); using UnmanagedMemoryStream stream = new(ptr, buffer.Length);
@ -99,6 +111,13 @@ public abstract partial class Image
/// <exception cref="UnknownImageFormatException">The encoded image format is unknown.</exception> /// <exception cref="UnknownImageFormatException">The encoded image format is unknown.</exception>
public static unsafe Image Load(DecoderOptions options, ReadOnlySpan<byte> buffer) public static unsafe Image Load(DecoderOptions options, ReadOnlySpan<byte> buffer)
{ {
Guard.NotNull(options, nameof(options));
if (buffer.IsEmpty)
{
throw new UnknownImageFormatException("Cannot load image from empty data.");
}
fixed (byte* ptr = buffer) fixed (byte* ptr = buffer)
{ {
using UnmanagedMemoryStream stream = new(ptr, buffer.Length); using UnmanagedMemoryStream stream = new(ptr, buffer.Length);
@ -133,6 +152,13 @@ public abstract partial class Image
public static unsafe Image<TPixel> Load<TPixel>(DecoderOptions options, ReadOnlySpan<byte> data) public static unsafe Image<TPixel> Load<TPixel>(DecoderOptions options, ReadOnlySpan<byte> data)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Guard.NotNull(options, nameof(options));
if (data.IsEmpty)
{
throw new UnknownImageFormatException("Cannot load image from empty data.");
}
fixed (byte* ptr = data) fixed (byte* ptr = data)
{ {
using UnmanagedMemoryStream stream = new(ptr, data.Length); using UnmanagedMemoryStream stream = new(ptr, data.Length);

5
src/ImageSharp/Image.LoadPixelData.cs

@ -69,6 +69,11 @@ public abstract partial class Image
{ {
Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(configuration, nameof(configuration));
if (data.IsEmpty)
{
throw new ArgumentException("Pixel data cannot be empty.", nameof(data));
}
int count = width * height; int count = width * height;
Guard.MustBeGreaterThanOrEqualTo(data.Length, count, nameof(data)); Guard.MustBeGreaterThanOrEqualTo(data.Length, count, nameof(data));

2
src/ImageSharp/ImageSharp.csproj

@ -30,7 +30,7 @@
<Choose> <Choose>
<When Condition="$(SIXLABORS_TESTING_PREVIEW) == true"> <When Condition="$(SIXLABORS_TESTING_PREVIEW) == true">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks> <TargetFrameworks>net8.0;net10.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
</When> </When>
<Otherwise> <Otherwise>

14
src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs

@ -318,7 +318,7 @@ public sealed class ExifProfile : IDeepCloneable<ExifProfile>
{ {
if (location.Value?.Length == 2) if (location.Value?.Length == 2)
{ {
Vector2 point = TransformUtils.ProjectiveTransform2D(location.Value[0], location.Value[1], matrix); Vector2 point = TransformUtilities.ProjectiveTransform2D(location.Value[0], location.Value[1], matrix);
// Ensure the point is within the image dimensions. // Ensure the point is within the image dimensions.
point = Vector2.Clamp(point, Vector2.Zero, new Vector2(width - 1, height - 1)); point = Vector2.Clamp(point, Vector2.Zero, new Vector2(width - 1, height - 1));
@ -340,18 +340,18 @@ public sealed class ExifProfile : IDeepCloneable<ExifProfile>
if (area.Value?.Length == 4) if (area.Value?.Length == 4)
{ {
RectangleF rectangle = new(area.Value[0], area.Value[1], area.Value[2], area.Value[3]); RectangleF rectangle = new(area.Value[0], area.Value[1], area.Value[2], area.Value[3]);
if (!TransformUtils.TryGetTransformedRectangle(rectangle, matrix, out Rectangle bounds)) if (!TransformUtilities.TryGetTransformedRectangle(rectangle, matrix, out RectangleF bounds))
{ {
return; return;
} }
// Ensure the bounds are within the image dimensions. // Ensure the bounds are within the image dimensions.
bounds = Rectangle.Intersect(bounds, new Rectangle(0, 0, width, height)); bounds = RectangleF.Intersect(bounds, new Rectangle(0, 0, width, height));
area.Value[0] = (ushort)bounds.X; area.Value[0] = (ushort)MathF.Floor(bounds.X);
area.Value[1] = (ushort)bounds.Y; area.Value[1] = (ushort)MathF.Floor(bounds.Y);
area.Value[2] = (ushort)bounds.Width; area.Value[2] = (ushort)MathF.Ceiling(bounds.Width);
area.Value[3] = (ushort)bounds.Height; area.Value[3] = (ushort)MathF.Ceiling(bounds.Height);
this.SetValue(ExifTag.SubjectArea, area.Value); this.SetValue(ExifTag.SubjectArea, area.Value);
} }
else else

12
src/ImageSharp/Primitives/Point.cs

@ -69,7 +69,7 @@ public struct Point : IEquatable<Point>
/// Gets a value indicating whether this <see cref="Point"/> is empty. /// Gets a value indicating whether this <see cref="Point"/> is empty.
/// </summary> /// </summary>
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]
public bool IsEmpty => this.Equals(Empty); public readonly bool IsEmpty => this.Equals(Empty);
/// <summary> /// <summary>
/// Creates a <see cref="PointF"/> with the coordinates of the specified <see cref="Point"/>. /// Creates a <see cref="PointF"/> with the coordinates of the specified <see cref="Point"/>.
@ -239,7 +239,7 @@ public struct Point : IEquatable<Point>
/// </summary> /// </summary>
/// <param name="x">The out value for X.</param> /// <param name="x">The out value for X.</param>
/// <param name="y">The out value for Y.</param> /// <param name="y">The out value for Y.</param>
public void Deconstruct(out int x, out int y) public readonly void Deconstruct(out int x, out int y)
{ {
x = this.X; x = this.X;
y = this.Y; y = this.Y;
@ -268,17 +268,17 @@ public struct Point : IEquatable<Point>
public void Offset(Point point) => this.Offset(point.X, point.Y); public void Offset(Point point) => this.Offset(point.X, point.Y);
/// <inheritdoc/> /// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.X, this.Y); public override readonly int GetHashCode() => HashCode.Combine(this.X, this.Y);
/// <inheritdoc/> /// <inheritdoc/>
public override string ToString() => $"Point [ X={this.X}, Y={this.Y} ]"; public override readonly string ToString() => $"Point [ X={this.X}, Y={this.Y} ]";
/// <inheritdoc/> /// <inheritdoc/>
public override bool Equals(object? obj) => obj is Point other && this.Equals(other); public override readonly bool Equals(object? obj) => obj is Point other && this.Equals(other);
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Point other) => this.X.Equals(other.X) && this.Y.Equals(other.Y); public readonly bool Equals(Point other) => this.X.Equals(other.X) && this.Y.Equals(other.Y);
private static short HighInt16(int n) => unchecked((short)((n >> 16) & 0xffff)); private static short HighInt16(int n) => unchecked((short)((n >> 16) & 0xffff));

12
src/ImageSharp/Primitives/PointF.cs

@ -58,7 +58,7 @@ public struct PointF : IEquatable<PointF>
/// Gets a value indicating whether this <see cref="PointF"/> is empty. /// Gets a value indicating whether this <see cref="PointF"/> is empty.
/// </summary> /// </summary>
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]
public bool IsEmpty => this.Equals(Empty); public readonly bool IsEmpty => this.Equals(Empty);
/// <summary> /// <summary>
/// Creates a <see cref="Vector2"/> with the coordinates of the specified <see cref="PointF"/>. /// Creates a <see cref="Vector2"/> with the coordinates of the specified <see cref="PointF"/>.
@ -251,7 +251,7 @@ public struct PointF : IEquatable<PointF>
/// </summary> /// </summary>
/// <param name="x">The out value for X.</param> /// <param name="x">The out value for X.</param>
/// <param name="y">The out value for Y.</param> /// <param name="y">The out value for Y.</param>
public void Deconstruct(out float x, out float y) public readonly void Deconstruct(out float x, out float y)
{ {
x = this.X; x = this.X;
y = this.Y; y = this.Y;
@ -277,15 +277,15 @@ public struct PointF : IEquatable<PointF>
public void Offset(PointF point) => this.Offset(point.X, point.Y); public void Offset(PointF point) => this.Offset(point.X, point.Y);
/// <inheritdoc/> /// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.X, this.Y); public override readonly int GetHashCode() => HashCode.Combine(this.X, this.Y);
/// <inheritdoc/> /// <inheritdoc/>
public override string ToString() => $"PointF [ X={this.X}, Y={this.Y} ]"; public override readonly string ToString() => $"PointF [ X={this.X}, Y={this.Y} ]";
/// <inheritdoc/> /// <inheritdoc/>
public override bool Equals(object? obj) => obj is PointF pointF && this.Equals(pointF); public override readonly bool Equals(object? obj) => obj is PointF pointF && this.Equals(pointF);
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(PointF other) => this.X.Equals(other.X) && this.Y.Equals(other.Y); public readonly bool Equals(PointF other) => this.X.Equals(other.X) && this.Y.Equals(other.Y);
} }

12
src/ImageSharp/Primitives/SizeF.cs

@ -67,7 +67,7 @@ public struct SizeF : IEquatable<SizeF>
/// Gets a value indicating whether this <see cref="SizeF"/> is empty. /// Gets a value indicating whether this <see cref="SizeF"/> is empty.
/// </summary> /// </summary>
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]
public bool IsEmpty => this.Equals(Empty); public readonly bool IsEmpty => this.Equals(Empty);
/// <summary> /// <summary>
/// Creates a <see cref="Vector2"/> with the coordinates of the specified <see cref="PointF"/>. /// Creates a <see cref="Vector2"/> with the coordinates of the specified <see cref="PointF"/>.
@ -201,24 +201,24 @@ public struct SizeF : IEquatable<SizeF>
/// </summary> /// </summary>
/// <param name="width">The out value for the width.</param> /// <param name="width">The out value for the width.</param>
/// <param name="height">The out value for the height.</param> /// <param name="height">The out value for the height.</param>
public void Deconstruct(out float width, out float height) public readonly void Deconstruct(out float width, out float height)
{ {
width = this.Width; width = this.Width;
height = this.Height; height = this.Height;
} }
/// <inheritdoc/> /// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.Width, this.Height); public override readonly int GetHashCode() => HashCode.Combine(this.Width, this.Height);
/// <inheritdoc/> /// <inheritdoc/>
public override string ToString() => $"SizeF [ Width={this.Width}, Height={this.Height} ]"; public override readonly string ToString() => $"SizeF [ Width={this.Width}, Height={this.Height} ]";
/// <inheritdoc/> /// <inheritdoc/>
public override bool Equals(object? obj) => obj is SizeF && this.Equals((SizeF)obj); public override readonly bool Equals(object? obj) => obj is SizeF sizeF && this.Equals(sizeF);
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(SizeF other) => this.Width.Equals(other.Width) && this.Height.Equals(other.Height); public readonly bool Equals(SizeF other) => this.Width.Equals(other.Width) && this.Height.Equals(other.Height);
/// <summary> /// <summary>
/// Multiplies <see cref="SizeF"/> by a <see cref="float"/> producing <see cref="SizeF"/>. /// Multiplies <see cref="SizeF"/> by a <see cref="float"/> producing <see cref="SizeF"/>.

43
src/ImageSharp/Processing/AffineTransformBuilder.cs

@ -17,24 +17,9 @@ public class AffineTransformBuilder
/// Initializes a new instance of the <see cref="AffineTransformBuilder"/> class. /// Initializes a new instance of the <see cref="AffineTransformBuilder"/> class.
/// </summary> /// </summary>
public AffineTransformBuilder() public AffineTransformBuilder()
: this(TransformSpace.Pixel)
{ {
} }
/// <summary>
/// Initializes a new instance of the <see cref="AffineTransformBuilder"/> class.
/// </summary>
/// <param name="transformSpace">
/// The <see cref="TransformSpace"/> to use when applying the affine transform.
/// </param>
public AffineTransformBuilder(TransformSpace transformSpace)
=> this.TransformSpace = transformSpace;
/// <summary>
/// Gets the <see cref="TransformSpace"/> to use when applying the affine transform.
/// </summary>
public TransformSpace TransformSpace { get; }
/// <summary> /// <summary>
/// Prepends a rotation matrix using the given rotation angle in degrees /// Prepends a rotation matrix using the given rotation angle in degrees
/// and the image center point as rotation center. /// and the image center point as rotation center.
@ -52,7 +37,7 @@ public class AffineTransformBuilder
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns> /// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependRotationRadians(float radians) public AffineTransformBuilder PrependRotationRadians(float radians)
=> this.Prepend( => this.Prepend(
size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace)); size => TransformUtilities.CreateRotationTransformMatrixRadians(radians, size));
/// <summary> /// <summary>
/// Prepends a rotation matrix using the given rotation in degrees at the given origin. /// Prepends a rotation matrix using the given rotation in degrees at the given origin.
@ -88,7 +73,7 @@ public class AffineTransformBuilder
/// <param name="radians">The amount of rotation, in radians.</param> /// <param name="radians">The amount of rotation, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns> /// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendRotationRadians(float radians) public AffineTransformBuilder AppendRotationRadians(float radians)
=> this.Append(size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace)); => this.Append(size => TransformUtilities.CreateRotationTransformMatrixRadians(radians, size));
/// <summary> /// <summary>
/// Appends a rotation matrix using the given rotation in degrees at the given origin. /// Appends a rotation matrix using the given rotation in degrees at the given origin.
@ -172,7 +157,7 @@ public class AffineTransformBuilder
/// <param name="radiansY">The Y angle, in radians.</param> /// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns> /// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY) public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY)
=> this.Prepend(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace)); => this.Prepend(size => TransformUtilities.CreateSkewTransformMatrixRadians(radiansX, radiansY, size));
/// <summary> /// <summary>
/// Prepends a skew matrix using the given angles in degrees at the given origin. /// Prepends a skew matrix using the given angles in degrees at the given origin.
@ -210,7 +195,7 @@ public class AffineTransformBuilder
/// <param name="radiansY">The Y angle, in radians.</param> /// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns> /// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY) public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY)
=> this.Append(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace)); => this.Append(size => TransformUtilities.CreateSkewTransformMatrixRadians(radiansX, radiansY, size));
/// <summary> /// <summary>
/// Appends a skew matrix using the given angles in degrees at the given origin. /// Appends a skew matrix using the given angles in degrees at the given origin.
@ -344,15 +329,29 @@ public class AffineTransformBuilder
/// for linear transforms. /// for linear transforms.
/// </exception> /// </exception>
/// <returns>The <see cref="Size"/>.</returns> /// <returns>The <see cref="Size"/>.</returns>
public Size GetTransformedSize(Rectangle sourceRectangle) public SizeF GetTransformedSize(Rectangle sourceRectangle)
{ {
Matrix3x2 matrix = this.BuildMatrix(sourceRectangle); Matrix3x2 matrix = this.BuildMatrix(sourceRectangle);
return TransformUtils.GetTransformedSize(matrix, sourceRectangle.Size, this.TransformSpace); return GetTransformedSize(sourceRectangle, matrix);
} }
/// <summary>
/// Returns the size of a rectangle large enough to contain the transformed source rectangle.
/// </summary>
/// <param name="sourceRectangle">The rectangle in the source image.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <exception cref="DegenerateTransformException">
/// The resultant matrix is degenerate containing one or more values equivalent
/// to <see cref="float.NaN"/> or a zero determinant and therefore cannot be used
/// for linear transforms.
/// </exception>
/// <returns>The <see cref="Size"/>.</returns>
internal static SizeF GetTransformedSize(Rectangle sourceRectangle, Matrix3x2 matrix)
=> TransformUtilities.GetRawTransformedSize(matrix, sourceRectangle.Size);
private static void CheckDegenerate(Matrix3x2 matrix) private static void CheckDegenerate(Matrix3x2 matrix)
{ {
if (TransformUtils.IsDegenerate(matrix)) if (TransformUtilities.IsDegenerate(matrix))
{ {
throw new DegenerateTransformException("Matrix is degenerate. Check input values."); throw new DegenerateTransformException("Matrix is degenerate. Check input values.");
} }

8
src/ImageSharp/Processing/Extensions/Transforms/TransformExtensions.cs

@ -7,8 +7,8 @@ using SixLabors.ImageSharp.Processing.Processors.Transforms;
namespace SixLabors.ImageSharp.Processing; namespace SixLabors.ImageSharp.Processing;
/// <summary> /// <summary>
/// Defines extensions that allow the application of composable transform operations on an <see cref="Image"/> /// Defines extensions that allow the application of composable transform operations
/// using Mutate/Clone. /// on an <see cref="IImageProcessingContext"/> using Mutate/Clone.
/// </summary> /// </summary>
public static class TransformExtensions public static class TransformExtensions
{ {
@ -51,7 +51,7 @@ public static class TransformExtensions
IResampler sampler) IResampler sampler)
{ {
Matrix3x2 transform = builder.BuildMatrix(sourceRectangle); Matrix3x2 transform = builder.BuildMatrix(sourceRectangle);
Size targetDimensions = builder.GetTransformedSize(sourceRectangle); Size targetDimensions = TransformUtilities.GetTransformedCanvasSize(transform, sourceRectangle.Size);
return source.Transform(sourceRectangle, transform, targetDimensions, sampler); return source.Transform(sourceRectangle, transform, targetDimensions, sampler);
} }
@ -113,7 +113,7 @@ public static class TransformExtensions
IResampler sampler) IResampler sampler)
{ {
Matrix4x4 transform = builder.BuildMatrix(sourceRectangle); Matrix4x4 transform = builder.BuildMatrix(sourceRectangle);
Size targetDimensions = builder.GetTransformedSize(sourceRectangle); Size targetDimensions = TransformUtilities.GetTransformedCanvasSize(transform, sourceRectangle.Size);
return source.Transform(sourceRectangle, transform, targetDimensions, sampler); return source.Transform(sourceRectangle, transform, targetDimensions, sampler);
} }

2
src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor.cs

@ -21,7 +21,7 @@ public class AffineTransformProcessor : CloningImageProcessor
Guard.NotNull(sampler, nameof(sampler)); Guard.NotNull(sampler, nameof(sampler));
Guard.MustBeValueType(sampler); Guard.MustBeValueType(sampler);
if (TransformUtils.IsDegenerate(matrix)) if (TransformUtilities.IsDegenerate(matrix))
{ {
throw new DegenerateTransformException("Matrix is degenerate. Check input values."); throw new DegenerateTransformException("Matrix is degenerate. Check input values.");
} }

4
src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs

@ -77,7 +77,9 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
return; return;
} }
// Convert from screen to world space. // All matrices are defined in normalized coordinate space so we need to convert to pixel space.
// After normalization we need to invert the matrix for correct sampling.
matrix = TransformUtilities.NormalizeToPixel(matrix);
Matrix3x2.Invert(matrix, out matrix); Matrix3x2.Invert(matrix, out matrix);
if (sampler is NearestNeighborResampler) if (sampler is NearestNeighborResampler)

2
src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor.cs

@ -21,7 +21,7 @@ public sealed class ProjectiveTransformProcessor : CloningImageProcessor
Guard.NotNull(sampler, nameof(sampler)); Guard.NotNull(sampler, nameof(sampler));
Guard.MustBeValueType(sampler); Guard.MustBeValueType(sampler);
if (TransformUtils.IsDegenerate(matrix)) if (TransformUtilities.IsDegenerate(matrix))
{ {
throw new DegenerateTransformException("Matrix is degenerate. Check input values."); throw new DegenerateTransformException("Matrix is degenerate. Check input values.");
} }

8
src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs

@ -75,7 +75,9 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
return; return;
} }
// Convert from screen to world space. // All matrices are defined in normalized coordinate space so we need to convert to pixel space.
// After normalization we need to invert the matrix for correct sampling.
matrix = TransformUtilities.NormalizeToPixel(matrix);
Matrix4x4.Invert(matrix, out matrix); Matrix4x4.Invert(matrix, out matrix);
if (sampler is NearestNeighborResampler) if (sampler is NearestNeighborResampler)
@ -135,7 +137,7 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
for (int x = 0; x < destinationRowSpan.Length; x++) for (int x = 0; x < destinationRowSpan.Length; x++)
{ {
Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, this.matrix); Vector2 point = TransformUtilities.ProjectiveTransform2D(x, y, this.matrix);
int px = (int)MathF.Round(point.X); int px = (int)MathF.Round(point.X);
int py = (int)MathF.Round(point.Y); int py = (int)MathF.Round(point.Y);
@ -207,7 +209,7 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
for (int x = 0; x < span.Length; x++) for (int x = 0; x < span.Length; x++)
{ {
Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, matrix); Vector2 point = TransformUtilities.ProjectiveTransform2D(x, y, matrix);
float pY = point.Y; float pY = point.Y;
float pX = point.X; float pX = point.X;

4
src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs

@ -28,14 +28,14 @@ public sealed class RotateProcessor : AffineTransformProcessor
/// <param name="sourceSize">The source image size</param> /// <param name="sourceSize">The source image size</param>
public RotateProcessor(float degrees, IResampler sampler, Size sourceSize) public RotateProcessor(float degrees, IResampler sampler, Size sourceSize)
: this( : this(
TransformUtils.CreateRotationTransformMatrixDegrees(degrees, sourceSize, TransformSpace.Pixel), TransformUtilities.CreateRotationTransformMatrixDegrees(degrees, sourceSize),
sampler, sampler,
sourceSize) sourceSize)
=> this.Degrees = degrees; => this.Degrees = degrees;
// Helper constructor // Helper constructor
private RotateProcessor(Matrix3x2 rotationMatrix, IResampler sampler, Size sourceSize) private RotateProcessor(Matrix3x2 rotationMatrix, IResampler sampler, Size sourceSize)
: base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(rotationMatrix, sourceSize, TransformSpace.Pixel)) : base(rotationMatrix, sampler, TransformUtilities.GetTransformedCanvasSize(rotationMatrix, sourceSize))
{ {
} }

4
src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs

@ -30,7 +30,7 @@ public sealed class SkewProcessor : AffineTransformProcessor
/// <param name="sourceSize">The source image size</param> /// <param name="sourceSize">The source image size</param>
public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize) public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize)
: this( : this(
TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize, TransformSpace.Pixel), TransformUtilities.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize),
sampler, sampler,
sourceSize) sourceSize)
{ {
@ -40,7 +40,7 @@ public sealed class SkewProcessor : AffineTransformProcessor
// Helper constructor: // Helper constructor:
private SkewProcessor(Matrix3x2 skewMatrix, IResampler sampler, Size sourceSize) private SkewProcessor(Matrix3x2 skewMatrix, IResampler sampler, Size sourceSize)
: base(skewMatrix, sampler, TransformUtils.GetTransformedSize(skewMatrix, sourceSize, TransformSpace.Pixel)) : base(skewMatrix, sampler, TransformUtilities.GetTransformedCanvasSize(skewMatrix, sourceSize))
{ {
} }

2
src/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler,TPixel}.cs

@ -24,7 +24,7 @@ internal class SwizzleProcessor<TSwizzler, TPixel> : TransformProcessor<TPixel>
// Calculate the transform matrix from the swizzle operation to allow us // Calculate the transform matrix from the swizzle operation to allow us
// to update any metadata that represents pixel coordinates in the source image. // to update any metadata that represents pixel coordinates in the source image.
this.transformMatrix = new ProjectiveTransformBuilder() this.transformMatrix = new ProjectiveTransformBuilder()
.AppendMatrix(TransformUtils.GetSwizzlerMatrix(swizzler, sourceRectangle)) .AppendMatrix(TransformUtilities.GetSwizzlerMatrix(swizzler, sourceRectangle))
.BuildMatrix(sourceRectangle); .BuildMatrix(sourceRectangle);
} }

376
src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs → src/ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms;
/// <summary> /// <summary>
/// Contains utility methods for working with transforms. /// Contains utility methods for working with transforms.
/// </summary> /// </summary>
internal static class TransformUtils internal static class TransformUtilities
{ {
/// <summary> /// <summary>
/// Returns a value that indicates whether the specified matrix is degenerate /// Returns a value that indicates whether the specified matrix is degenerate
@ -80,79 +80,69 @@ internal static class TransformUtils
} }
/// <summary> /// <summary>
/// Creates a centered rotation transform matrix using the given rotation in degrees and the source size. /// Creates a centered rotation transform matrix using the given rotation in degrees and the original source size.
/// </summary> /// </summary>
/// <param name="degrees">The amount of rotation, in degrees.</param> /// <param name="degrees">The amount of rotation, in degrees.</param>
/// <param name="size">The source image size.</param> /// <param name="size">The source image size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when creating the centered matrix.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns> /// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size, TransformSpace transformSpace) public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size)
=> CreateRotationTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degrees), size, transformSpace); => CreateRotationTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degrees), size);
/// <summary> /// <summary>
/// Creates a centered rotation transform matrix using the given rotation in radians and the source size. /// Creates a centered rotation transform matrix using the given rotation in radians and the original source size.
/// </summary> /// </summary>
/// <param name="radians">The amount of rotation, in radians.</param> /// <param name="radians">The amount of rotation, in radians.</param>
/// <param name="size">The source image size.</param> /// <param name="size">The source image size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when creating the centered matrix.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns> /// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size, TransformSpace transformSpace) public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size)
=> CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotation(radians, PointF.Empty), size, transformSpace); => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotation(radians, PointF.Empty), size);
/// <summary> /// <summary>
/// Creates a centered skew transform matrix from the give angles in degrees and the source size. /// Creates a centered skew transform matrix from the give angles in degrees and the original source size.
/// </summary> /// </summary>
/// <param name="degreesX">The X angle, in degrees.</param> /// <param name="degreesX">The X angle, in degrees.</param>
/// <param name="degreesY">The Y angle, in degrees.</param> /// <param name="degreesY">The Y angle, in degrees.</param>
/// <param name="size">The source image size.</param> /// <param name="size">The source image size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when creating the centered matrix.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns> /// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size, TransformSpace transformSpace) public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size)
=> CreateSkewTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), size, transformSpace); => CreateSkewTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), size);
/// <summary> /// <summary>
/// Creates a centered skew transform matrix from the give angles in radians and the source size. /// Creates a centered skew transform matrix from the give angles in radians and the original source size.
/// </summary> /// </summary>
/// <param name="radiansX">The X angle, in radians.</param> /// <param name="radiansX">The X angle, in radians.</param>
/// <param name="radiansY">The Y angle, in radians.</param> /// <param name="radiansY">The Y angle, in radians.</param>
/// <param name="size">The source image size.</param> /// <param name="size">The source image size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when creating the centered matrix.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns> /// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size, TransformSpace transformSpace) public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size)
=> CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty), size, transformSpace); => CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty), size);
/// <summary> /// <summary>
/// Gets the centered transform matrix based upon the source rectangle. /// Gets the centered transform matrix based upon the source rectangle.
/// </summary> /// </summary>
/// <param name="matrix">The transformation matrix.</param> /// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source image size.</param> /// <param name="size">The source image size.</param>
/// <param name="transformSpace">
/// The <see cref="TransformSpace"/> to use when creating the centered matrix.
/// </param>
/// <returns>The <see cref="Matrix3x2"/></returns> /// <returns>The <see cref="Matrix3x2"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateCenteredTransformMatrix(Matrix3x2 matrix, Size size, TransformSpace transformSpace) public static Matrix3x2 CreateCenteredTransformMatrix(Matrix3x2 matrix, Size size)
{ {
Size transformSize = GetUnboundedTransformedSize(matrix, size, transformSpace); // 1) Unbounded size.
SizeF ts = GetRawTransformedSize(matrix, size);
// We invert the matrix to handle the transformation from screen to world space.
// This ensures scaling matrices are correct.
Matrix3x2.Invert(matrix, out Matrix3x2 inverted);
// The source size is provided using the coordinate space of the source image. // 2) Invert the content transform for screen->world.
// however the transform should always be applied in the pixel space. Matrix3x2.Invert(matrix, out Matrix3x2 inv);
// To account for this we offset by the size - 1 to translate to the pixel space.
float offset = transformSpace == TransformSpace.Pixel ? 1F : 0F;
Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-(transformSize.Width - offset), -(transformSize.Height - offset)) * .5F); // 3) Translate target (canvas) so its center is at the origin,
Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(size.Width - offset, size.Height - offset) * .5F); // translate source so its center is at the origin, then undo the content transform.
Matrix3x2 toTarget = Matrix3x2.CreateTranslation(new Vector2(-ts.Width, -ts.Height) * 0.5f);
Matrix3x2 toSource = Matrix3x2.CreateTranslation(new Vector2(size.Width, size.Height) * 0.5f);
// Translate back to world space. // 4) World->screen.
Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); Matrix3x2.Invert(toTarget * inv * toSource, out Matrix3x2 centered);
return centered; return centered;
} }
@ -287,7 +277,6 @@ internal static class TransformUtils
/// <param name="topRight">The top-right point of the distorted quad.</param> /// <param name="topRight">The top-right point of the distorted quad.</param>
/// <param name="bottomRight">The bottom-right point of the distorted quad.</param> /// <param name="bottomRight">The bottom-right point of the distorted quad.</param>
/// <param name="bottomLeft">The bottom-left point of the distorted quad.</param> /// <param name="bottomLeft">The bottom-left point of the distorted quad.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when creating the matrix.</param>
/// <returns>The computed projection matrix for the quad distortion.</returns> /// <returns>The computed projection matrix for the quad distortion.</returns>
/// <remarks> /// <remarks>
/// This method is based on the algorithm described in the following article: /// This method is based on the algorithm described in the following article:
@ -298,8 +287,7 @@ internal static class TransformUtils
PointF topLeft, PointF topLeft,
PointF topRight, PointF topRight,
PointF bottomRight, PointF bottomRight,
PointF bottomLeft, PointF bottomLeft)
TransformSpace transformSpace)
{ {
PointF p1 = new(rectangle.X, rectangle.Y); PointF p1 = new(rectangle.X, rectangle.Y);
PointF p2 = new(rectangle.X + rectangle.Width, rectangle.Y); PointF p2 = new(rectangle.X + rectangle.Width, rectangle.Y);
@ -345,46 +333,94 @@ internal static class TransformUtils
(float)b[2], (float)b[5], 0, 1); (float)b[2], (float)b[5], 0, 1);
#pragma warning restore SA1117 #pragma warning restore SA1117
// Check if the matrix involves only affine transformations by inspecting the relevant components.
// We want to use pixel space for calculations only if the transformation is purely 2D and does not include
// any perspective effects, non-standard scaling, or unusual translations that could distort the image.
if (transformSpace == TransformSpace.Pixel && IsAffineRotationOrSkew(projectionMatrix))
{
if (projectionMatrix.M41 != 0)
{
projectionMatrix.M41--;
}
if (projectionMatrix.M42 != 0)
{
projectionMatrix.M42--;
}
}
return projectionMatrix; return projectionMatrix;
} }
/// <summary> /// <summary>
/// Returns the size relative to the source for the given transformation matrix. /// Calculates the size of a destination canvas large enough to contain
/// the fully transformed source content, including any translation offsets.
/// </summary> /// </summary>
/// <param name="matrix">The transformation matrix.</param> /// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param> /// <param name="size">The original source size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when calculating the size.</param> /// <returns>
/// <returns>The <see cref="Size"/>.</returns> /// A <see cref="SizeF"/> representing the dimensions of the destination
public static Size GetTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace) /// canvas required to fully contain the transformed source, including
=> GetTransformedSize(matrix, size, transformSpace, true); /// any positive or negative translation offsets.
/// </returns>
/// <remarks>
/// <para>
/// This method ensures that the transformed content remains fully visible
/// on the destination canvas by expanding its size to include translations
/// in all directions.
/// </para>
/// <para>
/// It behaves identically to calling
/// <see cref="GetTransformedSize(Matrix3x2, Size, bool)"/> with
/// <c>preserveCanvas</c> set to <see langword="true"/>.
/// </para>
/// <para>
/// The resulting canvas size represents the total area required to display
/// the transformed image without clipping, not merely the geometric bounds
/// of the transformed source.
/// </para>
/// </remarks>
public static Size GetTransformedCanvasSize(Matrix3x2 matrix, Size size)
=> Size.Ceiling(GetTransformedSize(matrix, size, true));
/// <summary> /// <summary>
/// Returns the size relative to the source for the given transformation matrix. /// Calculates the size of a destination canvas large enough to contain
/// the fully transformed source content, including any translation offsets.
/// </summary> /// </summary>
/// <param name="matrix">The transformation matrix.</param> /// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param> /// <param name="size">The original source size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> used when generating the matrix.</param>
/// <returns> /// <returns>
/// The <see cref="Size"/>. /// A <see cref="SizeF"/> representing the dimensions of the destination
/// canvas required to fully contain the transformed source, including
/// any positive or negative translation offsets.
/// </returns> /// </returns>
/// <remarks>
/// <para>
/// This method ensures that the transformed content remains fully visible
/// on the destination canvas by expanding its size to include translations
/// in all directions.
/// </para>
/// <para>
/// It behaves identically to calling
/// <see cref="GetTransformedSize(Matrix3x2, Size, bool)"/> with
/// <c>preserveCanvas</c> set to <see langword="true"/>.
/// </para>
/// <para>
/// The resulting canvas size represents the total area required to display
/// the transformed image without clipping, not merely the geometric bounds
/// of the transformed source.
/// </para>
/// </remarks>
public static Size GetTransformedCanvasSize(Matrix4x4 matrix, Size size)
=> Size.Ceiling(GetTransformedSize(matrix, size, true));
/// <summary>
/// Returns the size relative to the source for the given transformation matrix.
/// </summary>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The original source size.</param>
/// <returns>The <see cref="Size"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SizeF GetRawTransformedSize(Matrix4x4 matrix, Size size)
=> GetTransformedSize(matrix, size, false);
/// <summary>
/// Returns the size of the transformed source. When <paramref name="preserveCanvas"/> is true,
/// the size is expanded to include translation so the full moved content remains visible.
/// </summary>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The original source size.</param>
/// <param name="preserveCanvas">
/// If <see langword="true"/>, expand the size to account for translation (left/up as well as right/down).
/// If <see langword="false"/>, return only the transformed span without translation expansion.
/// </param>
/// <returns>The <see cref="SizeF"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Size GetTransformedSize(Matrix4x4 matrix, Size size, TransformSpace transformSpace) private static SizeF GetTransformedSize(Matrix4x4 matrix, Size size, bool preserveCanvas)
{ {
Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!");
@ -393,27 +429,9 @@ internal static class TransformUtils
return size; return size;
} }
// Check if the matrix involves only affine transformations by inspecting the relevant components. if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size), matrix, out RectangleF bounds))
// We want to use pixel space for calculations only if the transformation is purely 2D and does not include
// any perspective effects, non-standard scaling, or unusual translations that could distort the image.
bool usePixelSpace = transformSpace == TransformSpace.Pixel && IsAffineRotationOrSkew(matrix);
// Define an offset size to translate between pixel space and coordinate space.
// When using pixel space, apply a scaling sensitive offset to translate to discrete pixel coordinates.
// When not using pixel space, use SizeF.Empty as the offset.
// Compute scaling factors from the matrix
float scaleX = 1F / new Vector2(matrix.M11, matrix.M21).Length(); // sqrt(M11^2 + M21^2)
float scaleY = 1F / new Vector2(matrix.M12, matrix.M22).Length(); // sqrt(M12^2 + M22^2)
// Apply the offset relative to the scale
SizeF offsetSize = usePixelSpace ? new SizeF(scaleX, scaleY) : SizeF.Empty;
// Subtract the offset size to translate to the appropriate space (pixel or coordinate).
if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size - offsetSize), matrix, out Rectangle bounds))
{ {
// Add the offset size back to translate the transformed bounds to the correct space. return preserveCanvas ? GetPreserveCanvasSize(bounds) : bounds.Size;
return Size.Ceiling(ConstrainSize(bounds) + offsetSize);
} }
return size; return size;
@ -438,30 +456,31 @@ internal static class TransformUtils
swizzler.Transform(new Point(sourceRectangle.Left, sourceRectangle.Top)), swizzler.Transform(new Point(sourceRectangle.Left, sourceRectangle.Top)),
swizzler.Transform(new Point(sourceRectangle.Right, sourceRectangle.Top)), swizzler.Transform(new Point(sourceRectangle.Right, sourceRectangle.Top)),
swizzler.Transform(new Point(sourceRectangle.Right, sourceRectangle.Bottom)), swizzler.Transform(new Point(sourceRectangle.Right, sourceRectangle.Bottom)),
swizzler.Transform(new Point(sourceRectangle.Left, sourceRectangle.Bottom)), swizzler.Transform(new Point(sourceRectangle.Left, sourceRectangle.Bottom)));
TransformSpace.Pixel);
/// <summary> /// <summary>
/// Returns the size relative to the source for the given transformation matrix. /// Returns the size relative to the source for the given transformation matrix.
/// </summary> /// </summary>
/// <param name="matrix">The transformation matrix.</param> /// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param> /// <param name="size">The original source size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when calculating the size.</param>
/// <returns>The <see cref="Size"/>.</returns> /// <returns>The <see cref="Size"/>.</returns>
private static Size GetUnboundedTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace) [MethodImpl(MethodImplOptions.AggressiveInlining)]
=> GetTransformedSize(matrix, size, transformSpace, false); public static SizeF GetRawTransformedSize(Matrix3x2 matrix, Size size)
=> GetTransformedSize(matrix, size, false);
/// <summary> /// <summary>
/// Returns the size relative to the source for the given transformation matrix. /// Returns the size of the transformed source. When <paramref name="preserveCanvas"/> is true,
/// the size is expanded to include translation so the full moved content remains visible.
/// </summary> /// </summary>
/// <param name="matrix">The transformation matrix.</param> /// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param> /// <param name="size">The original source size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when calculating the size.</param> /// <param name="preserveCanvas">
/// <param name="constrain">Whether to constrain the size to ensure that the dimensions are positive.</param> /// If <see langword="true"/>, expand the size to account for translation (left/up as well as right/down).
/// <returns> /// If <see langword="false"/>, return only the transformed span without translation expansion.
/// The <see cref="Size"/>. /// </param>
/// </returns> /// <returns>The <see cref="SizeF"/>.</returns>
private static Size GetTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace, bool constrain) [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static SizeF GetTransformedSize(Matrix3x2 matrix, Size size, bool preserveCanvas)
{ {
Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!");
@ -470,21 +489,9 @@ internal static class TransformUtils
return size; return size;
} }
// Define an offset size to translate between coordinate space and pixel space. if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size), matrix, out RectangleF bounds))
// Compute scaling factors from the matrix
SizeF offsetSize = SizeF.Empty;
if (transformSpace == TransformSpace.Pixel)
{
float scaleX = 1F / new Vector2(matrix.M11, matrix.M21).Length(); // sqrt(M11^2 + M21^2)
float scaleY = 1F / new Vector2(matrix.M12, matrix.M22).Length(); // sqrt(M12^2 + M22^2)
offsetSize = new SizeF(scaleX, scaleY);
}
// Subtract the offset size to translate to the pixel space.
if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size - offsetSize), matrix, out Rectangle bounds))
{ {
// Add the offset size back to translate the transformed bounds to the coordinate space. return preserveCanvas ? GetPreserveCanvasSize(bounds) : bounds.Size;
return Size.Ceiling((constrain ? ConstrainSize(bounds) : bounds.Size) + offsetSize);
} }
return size; return size;
@ -499,7 +506,8 @@ internal static class TransformUtils
/// <returns> /// <returns>
/// <see langword="true"/> if the transformation was successful; otherwise, <see langword="false"/>. /// <see langword="true"/> if the transformation was successful; otherwise, <see langword="false"/>.
/// </returns> /// </returns>
private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix3x2 matrix, out Rectangle bounds) [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix3x2 matrix, out RectangleF bounds)
{ {
if (matrix.IsIdentity || rectangle.Equals(default)) if (matrix.IsIdentity || rectangle.Equals(default))
{ {
@ -526,7 +534,7 @@ internal static class TransformUtils
/// <see langword="true"/> if the transformation was successful; otherwise, <see langword="false"/>. /// <see langword="true"/> if the transformation was successful; otherwise, <see langword="false"/>.
/// </returns> /// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix4x4 matrix, out Rectangle bounds) internal static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix4x4 matrix, out RectangleF bounds)
{ {
if (matrix.IsIdentity || rectangle.Equals(default)) if (matrix.IsIdentity || rectangle.Equals(default))
{ {
@ -543,15 +551,61 @@ internal static class TransformUtils
return true; return true;
} }
/// <summary>
/// Calculates the size of a destination canvas large enough to contain the full
/// transformed content of a source rectangle while preserving any translation offsets.
/// </summary>
/// <param name="rectangle">
/// The <see cref="RectangleF"/> representing the transformed bounds of the source content
/// in destination (output) space.
/// </param>
/// <returns>
/// A <see cref="SizeF"/> that describes the canvas dimensions required to fully
/// contain the transformed content while accounting for any positive or negative translation.
/// </returns>
/// <remarks>
/// <para>
/// This method expands the output canvas to ensure that translated content remains visible.
/// </para>
/// <para>
/// If the transformation produces a positive translation, the method extends the canvas
/// on the positive side (right or bottom).
/// If the transformation produces a negative translation (the content moves left or up),
/// the method extends the canvas on the negative side to include that offset.
/// </para>
/// <para>
/// The result is equivalent to taking the union of:
/// <list type="bullet">
/// <item>
/// <description>The original, untransformed rectangle at the origin [0..Width] × [0..Height].</description>
/// </item>
/// <item>
/// <description>The translated rectangle defined by <paramref name="rectangle"/>.</description>
/// </item>
/// </list>
/// This ensures the entire translated image fits within the resulting canvas,
/// without trimming any portion caused by translation.
/// </para>
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Size ConstrainSize(Rectangle rectangle) private static SizeF GetPreserveCanvasSize(RectangleF rectangle)
{ {
// We want to resize the canvas here taking into account any translations. // Compute the required height.
int height = rectangle.Top < 0 ? rectangle.Bottom : Math.Max(rectangle.Height, rectangle.Bottom); // If the top is negative, expand upward by that amount (rectangle.Bottom already includes height).
int width = rectangle.Left < 0 ? rectangle.Right : Math.Max(rectangle.Width, rectangle.Right); // Otherwise, take the larger of the transformed height or the bottom offset.
float height = rectangle.Top < 0
// If location in either direction is translated to a negative value equal to or exceeding the ? rectangle.Bottom
// dimensions in either direction we need to reassign the dimension. : MathF.Max(rectangle.Height, rectangle.Bottom);
// Compute the required width.
// If the left is negative, expand leftward by that amount (rectangle.Right already includes width).
// Otherwise, take the larger of the transformed width or the right offset.
float width = rectangle.Left < 0
? rectangle.Right
: MathF.Max(rectangle.Width, rectangle.Right);
// Guard: if translation exceeds or cancels dimensions,
// ensure non-zero positive size using the base rectangle dimensions.
if (height <= 0) if (height <= 0)
{ {
height = rectangle.Height; height = rectangle.Height;
@ -562,63 +616,63 @@ internal static class TransformUtils
width = rectangle.Width; width = rectangle.Width;
} }
return new Size(width, height); // Return the final size that preserves the full visible region of the transformed content.
return new SizeF(width, height);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Rectangle GetBoundingRectangle(Vector2 tl, Vector2 tr, Vector2 bl, Vector2 br) private static RectangleF GetBoundingRectangle(Vector2 tl, Vector2 tr, Vector2 bl, Vector2 br)
{ {
// Find the minimum and maximum "corners" based on the given vectors
float left = MathF.Min(tl.X, MathF.Min(tr.X, MathF.Min(bl.X, br.X))); float left = MathF.Min(tl.X, MathF.Min(tr.X, MathF.Min(bl.X, br.X)));
float top = MathF.Min(tl.Y, MathF.Min(tr.Y, MathF.Min(bl.Y, br.Y))); float top = MathF.Min(tl.Y, MathF.Min(tr.Y, MathF.Min(bl.Y, br.Y)));
float right = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X))); float right = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X)));
float bottom = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y))); float bottom = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y)));
// Clamp the values to the nearest whole pixel. return RectangleF.FromLTRB(left, top, right, bottom);
return Rectangle.FromLTRB(
(int)Math.Floor(left),
(int)Math.Floor(top),
(int)Math.Ceiling(right),
(int)Math.Ceiling(bottom));
} }
private static bool IsAffineRotationOrSkew(Matrix4x4 matrix) /// <summary>
/// Normalizes an affine 2D matrix so that it operates in pixel space.
/// Applies the row-vector conjugation <c>T(+0.5,+0.5) * M * T(-0.5,-0.5)</c>
/// to align the transform with pixel centers.
/// </summary>
/// <param name="matrix">The affine matrix.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 NormalizeToPixel(Matrix3x2 matrix)
{ {
const float epsilon = 1e-6f; const float dx = 0.5f, dy = 0.5f;
// Check if the matrix is affine (last column should be [0, 0, 0, 1]) matrix.M31 += (-dx) + ((dx * matrix.M11) + (dy * matrix.M21));
if (Math.Abs(matrix.M14) > epsilon || matrix.M32 += (-dy) + ((dx * matrix.M12) + (dy * matrix.M22));
Math.Abs(matrix.M24) > epsilon || return matrix;
Math.Abs(matrix.M34) > epsilon || }
Math.Abs(matrix.M44 - 1f) > epsilon)
{
return false;
}
// Translation component (M41, m42) are allowed, others are not.
if (Math.Abs(matrix.M43) > epsilon)
{
return false;
}
// Extract the linear (rotation and skew) part of the matrix
// Upper-left 3x3 matrix
float m11 = matrix.M11, m12 = matrix.M12, m13 = matrix.M13;
float m21 = matrix.M21, m22 = matrix.M22, m23 = matrix.M23;
float m31 = matrix.M31, m32 = matrix.M32, m33 = matrix.M33;
// Compute the determinant of the linear part /// <summary>
float determinant = (m11 * ((m22 * m33) - (m23 * m32))) - /// Normalizes a projective 4×4 matrix so that it operates in pixel space.
(m12 * ((m21 * m33) - (m23 * m31))) + /// Applies the row-vector conjugation <c>T(+0.5,+0.5,0) * M * T(-0.5,-0.5,0)</c>
(m13 * ((m21 * m32) - (m22 * m31))); /// to align the transform with pixel centers.
/// </summary>
/// <param name="matrix">The projective matrix.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix4x4 NormalizeToPixel(Matrix4x4 matrix)
{
const float dx = 0.5f, dy = 0.5f;
// Check if the determinant is approximately ±1 (no scaling) // Fast path: affine (no perspective)
if (Math.Abs(Math.Abs(determinant) - 1f) > epsilon) if (matrix.M14 == 0f && matrix.M24 == 0f && matrix.M34 == 0f && matrix.M44 == 1f)
{ {
return false; // t' = t + (-d + d·L)
matrix.M41 += (-dx) + ((dx * matrix.M11) + (dy * matrix.M21));
matrix.M42 += (-dy) + ((dx * matrix.M12) + (dy * matrix.M22));
return matrix;
} }
// All checks passed; the matrix represents rotation and/or skew (with possible translation) Matrix4x4 tPos = Matrix4x4.Identity;
return true; tPos.M41 = dx;
tPos.M42 = dy;
Matrix4x4 tNeg = Matrix4x4.Identity;
tNeg.M41 = -dx;
tNeg.M42 = -dy;
return tPos * matrix * tNeg;
} }
} }

66
src/ImageSharp/Processing/ProjectiveTransformBuilder.cs

@ -17,24 +17,9 @@ public class ProjectiveTransformBuilder
/// Initializes a new instance of the <see cref="ProjectiveTransformBuilder"/> class. /// Initializes a new instance of the <see cref="ProjectiveTransformBuilder"/> class.
/// </summary> /// </summary>
public ProjectiveTransformBuilder() public ProjectiveTransformBuilder()
: this(TransformSpace.Pixel)
{ {
} }
/// <summary>
/// Initializes a new instance of the <see cref="ProjectiveTransformBuilder"/> class.
/// </summary>
/// <param name="transformSpace">
/// The <see cref="TransformSpace"/> to use when applying the projective transform.
/// </param>
public ProjectiveTransformBuilder(TransformSpace transformSpace)
=> this.TransformSpace = transformSpace;
/// <summary>
/// Gets the <see cref="TransformSpace"/> to use when applying the projective transform.
/// </summary>
public TransformSpace TransformSpace { get; }
/// <summary> /// <summary>
/// Prepends a matrix that performs a tapering projective transform. /// Prepends a matrix that performs a tapering projective transform.
/// </summary> /// </summary>
@ -43,7 +28,7 @@ public class ProjectiveTransformBuilder
/// <param name="fraction">The amount to taper.</param> /// <param name="fraction">The amount to taper.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns> /// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependTaper(TaperSide side, TaperCorner corner, float fraction) public ProjectiveTransformBuilder PrependTaper(TaperSide side, TaperCorner corner, float fraction)
=> this.Prepend(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction)); => this.Prepend(size => TransformUtilities.CreateTaperMatrix(size, side, corner, fraction));
/// <summary> /// <summary>
/// Appends a matrix that performs a tapering projective transform. /// Appends a matrix that performs a tapering projective transform.
@ -53,7 +38,7 @@ public class ProjectiveTransformBuilder
/// <param name="fraction">The amount to taper.</param> /// <param name="fraction">The amount to taper.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns> /// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendTaper(TaperSide side, TaperCorner corner, float fraction) public ProjectiveTransformBuilder AppendTaper(TaperSide side, TaperCorner corner, float fraction)
=> this.Append(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction)); => this.Append(size => TransformUtilities.CreateTaperMatrix(size, side, corner, fraction));
/// <summary> /// <summary>
/// Prepends a centered rotation matrix using the given rotation in degrees. /// Prepends a centered rotation matrix using the given rotation in degrees.
@ -69,7 +54,7 @@ public class ProjectiveTransformBuilder
/// <param name="radians">The amount of rotation, in radians.</param> /// <param name="radians">The amount of rotation, in radians.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns> /// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependRotationRadians(float radians) public ProjectiveTransformBuilder PrependRotationRadians(float radians)
=> this.Prepend(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace))); => this.Prepend(size => new Matrix4x4(TransformUtilities.CreateRotationTransformMatrixRadians(radians, size)));
/// <summary> /// <summary>
/// Prepends a centered rotation matrix using the given rotation in degrees at the given origin. /// Prepends a centered rotation matrix using the given rotation in degrees at the given origin.
@ -87,7 +72,8 @@ public class ProjectiveTransformBuilder
/// <param name="origin">The rotation origin point.</param> /// <param name="origin">The rotation origin point.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns> /// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
internal ProjectiveTransformBuilder PrependRotationRadians(float radians, Vector2 origin) internal ProjectiveTransformBuilder PrependRotationRadians(float radians, Vector2 origin)
=> this.PrependMatrix(Matrix4x4.CreateRotationZ(radians, new Vector3(origin, 0))); => this.PrependMatrix(
Matrix4x4.CreateRotationZ(radians, new Vector3(origin, 0)));
/// <summary> /// <summary>
/// Appends a centered rotation matrix using the given rotation in degrees. /// Appends a centered rotation matrix using the given rotation in degrees.
@ -103,7 +89,7 @@ public class ProjectiveTransformBuilder
/// <param name="radians">The amount of rotation, in radians.</param> /// <param name="radians">The amount of rotation, in radians.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns> /// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendRotationRadians(float radians) public ProjectiveTransformBuilder AppendRotationRadians(float radians)
=> this.Append(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace))); => this.Append(size => new Matrix4x4(TransformUtilities.CreateRotationTransformMatrixRadians(radians, size)));
/// <summary> /// <summary>
/// Appends a centered rotation matrix using the given rotation in degrees at the given origin. /// Appends a centered rotation matrix using the given rotation in degrees at the given origin.
@ -187,7 +173,7 @@ public class ProjectiveTransformBuilder
/// <param name="radiansY">The Y angle, in radians.</param> /// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns> /// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY) public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY)
=> this.Prepend(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace))); => this.Prepend(size => new Matrix4x4(TransformUtilities.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)));
/// <summary> /// <summary>
/// Prepends a skew matrix using the given angles in degrees at the given origin. /// Prepends a skew matrix using the given angles in degrees at the given origin.
@ -225,7 +211,7 @@ public class ProjectiveTransformBuilder
/// <param name="radiansY">The Y angle, in radians.</param> /// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns> /// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY) public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY)
=> this.Append(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace))); => this.Append(size => new Matrix4x4(TransformUtilities.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)));
/// <summary> /// <summary>
/// Appends a skew matrix using the given angles in degrees at the given origin. /// Appends a skew matrix using the given angles in degrees at the given origin.
@ -288,8 +274,12 @@ public class ProjectiveTransformBuilder
/// <param name="bottomLeft">The bottom-left corner point of the distorted quad.</param> /// <param name="bottomLeft">The bottom-left corner point of the distorted quad.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns> /// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft) public ProjectiveTransformBuilder PrependQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft)
=> this.Prepend(size => TransformUtils.CreateQuadDistortionMatrix( => this.Prepend(size => TransformUtilities.CreateQuadDistortionMatrix(
new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft, this.TransformSpace)); new Rectangle(Point.Empty, size),
topLeft,
topRight,
bottomRight,
bottomLeft));
/// <summary> /// <summary>
/// Appends a quad distortion matrix using the specified corner points. /// Appends a quad distortion matrix using the specified corner points.
@ -300,8 +290,12 @@ public class ProjectiveTransformBuilder
/// <param name="bottomLeft">The bottom-left corner point of the distorted quad.</param> /// <param name="bottomLeft">The bottom-left corner point of the distorted quad.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns> /// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft) public ProjectiveTransformBuilder AppendQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft)
=> this.Append(size => TransformUtils.CreateQuadDistortionMatrix( => this.Append(size => TransformUtilities.CreateQuadDistortionMatrix(
new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft, this.TransformSpace)); new Rectangle(Point.Empty, size),
topLeft,
topRight,
bottomRight,
bottomLeft));
/// <summary> /// <summary>
/// Prepends a raw matrix. /// Prepends a raw matrix.
@ -383,15 +377,29 @@ public class ProjectiveTransformBuilder
/// for linear transforms. /// for linear transforms.
/// </exception> /// </exception>
/// <returns>The <see cref="Size"/>.</returns> /// <returns>The <see cref="Size"/>.</returns>
public Size GetTransformedSize(Rectangle sourceRectangle) public SizeF GetTransformedSize(Rectangle sourceRectangle)
{ {
Matrix4x4 matrix = this.BuildMatrix(sourceRectangle); Matrix4x4 matrix = this.BuildMatrix(sourceRectangle);
return TransformUtils.GetTransformedSize(matrix, sourceRectangle.Size, this.TransformSpace); return GetTransformedSize(sourceRectangle, matrix);
} }
/// <summary>
/// Returns the size of a rectangle large enough to contain the transformed source rectangle.
/// </summary>
/// <param name="sourceRectangle">The rectangle in the source image.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <exception cref="DegenerateTransformException">
/// The resultant matrix is degenerate containing one or more values equivalent
/// to <see cref="float.NaN"/> or a zero determinant and therefore cannot be used
/// for linear transforms.
/// </exception>
/// <returns>The <see cref="Size"/>.</returns>
internal static SizeF GetTransformedSize(Rectangle sourceRectangle, Matrix4x4 matrix)
=> TransformUtilities.GetRawTransformedSize(matrix, sourceRectangle.Size);
private static void CheckDegenerate(Matrix4x4 matrix) private static void CheckDegenerate(Matrix4x4 matrix)
{ {
if (TransformUtils.IsDegenerate(matrix)) if (TransformUtilities.IsDegenerate(matrix))
{ {
throw new DegenerateTransformException("Matrix is degenerate. Check input values."); throw new DegenerateTransformException("Matrix is degenerate. Check input values.");
} }

26
src/ImageSharp/Processing/TransformSpace.cs

@ -1,26 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Processing;
/// <summary>
/// Represents the different spaces used in transformation operations.
/// </summary>
public enum TransformSpace
{
/// <summary>
/// Coordinate space is a continuous, mathematical grid where objects and positions
/// are defined with precise, often fractional values. This space allows for fine-grained
/// transformations like scaling, rotation, and translation with high precision.
/// In coordinate space, an image can span from (0,0) to (4,4) for a 4x4 image, including the boundaries.
/// </summary>
Coordinate,
/// <summary>
/// Pixel space is a discrete grid where each position corresponds to a specific pixel on the screen.
/// In this space, positions are defined by whole numbers, with no fractional values.
/// A 4x4 image in pixel space covers exactly 4 pixels wide and 4 pixels tall, ranging from (0,0) to (3,3).
/// Pixel space is used when rendering images to ensure that everything aligns with the actual pixels on the screen.
/// </summary>
Pixel
}

2
tests/Directory.Build.targets

@ -25,7 +25,7 @@
See https://github.com/ImageMagick/ImageMagick/commit/27a0a9c37f18af9c8d823a3ea076f600843b553c See https://github.com/ImageMagick/ImageMagick/commit/27a0a9c37f18af9c8d823a3ea076f600843b553c
--> -->
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="13.10.0" /> <PackageReference Update="Magick.NET-Q16-AnyCPU" Version="13.10.0" />
<PackageReference Update="Microsoft.DotNet.RemoteExecutor" Version="8.0.0-beta.23580.1" /> <PackageReference Update="Microsoft.DotNet.RemoteExecutor" Version="10.0.0-beta.25563.105" />
<PackageReference Update="Microsoft.DotNet.XUnitExtensions" Version="8.0.0-beta.23580.1" /> <PackageReference Update="Microsoft.DotNet.XUnitExtensions" Version="8.0.0-beta.23580.1" />
<PackageReference Update="Moq" Version="4.20.72" /> <PackageReference Update="Moq" Version="4.20.72" />
<PackageReference Update="NetVips" Version="3.0.0" /> <PackageReference Update="NetVips" Version="3.0.0" />

4
tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj

@ -19,7 +19,7 @@
BenchmarkDotNet requires a certain structure to the code, BenchmarkDotNet requires a certain structure to the code,
as such, some of these rules cannot be implemented. as such, some of these rules cannot be implemented.
--> -->
<!--Mark members as static--> <!--Mark members as static-->
<!--Validate platform compatibility--> <!--Validate platform compatibility-->
<!--Types that own disposable fields should be disposable--> <!--Types that own disposable fields should be disposable-->
@ -39,7 +39,7 @@
<Choose> <Choose>
<When Condition="$(SIXLABORS_TESTING_PREVIEW) == true"> <When Condition="$(SIXLABORS_TESTING_PREVIEW) == true">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks> <TargetFrameworks>net8.0;net10.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
</When> </When>
<Otherwise> <Otherwise>

2
tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj

@ -19,7 +19,7 @@
<Choose> <Choose>
<When Condition="$(SIXLABORS_TESTING_PREVIEW) == true"> <When Condition="$(SIXLABORS_TESTING_PREVIEW) == true">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks> <TargetFrameworks>net8.0;net10.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
</When> </When>
<Otherwise> <Otherwise>

10
tests/ImageSharp.Tests/Common/SimdUtilsTests.Shuffle.cs

@ -292,7 +292,7 @@ public partial class SimdUtilsTests
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest, RunTest,
count, count,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableSSE); HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
} }
[Theory] [Theory]
@ -352,7 +352,7 @@ public partial class SimdUtilsTests
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest, RunTest,
count, count,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSE); HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic);
} }
[Theory] [Theory]
@ -394,7 +394,7 @@ public partial class SimdUtilsTests
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest, RunTest,
count, count,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSE); HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic);
} }
[Theory] [Theory]
@ -436,7 +436,7 @@ public partial class SimdUtilsTests
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest, RunTest,
count, count,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSE); HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic);
} }
[Theory] [Theory]
@ -478,7 +478,7 @@ public partial class SimdUtilsTests
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest, RunTest,
count, count,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableSSE); HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
} }
private static void TestShuffleFloat4Channel( private static void TestShuffleFloat4Channel(

4
tests/ImageSharp.Tests/Common/SimdUtilsTests.cs

@ -133,7 +133,7 @@ public partial class SimdUtilsTests
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest, RunTest,
count, count,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSE41); HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX2);
} }
[Theory] [Theory]
@ -171,7 +171,7 @@ public partial class SimdUtilsTests
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest, RunTest,
count, count,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512BW | HwIntrinsics.DisableAVX2); HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX2);
} }
[Theory] [Theory]

4
tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs

@ -267,7 +267,7 @@ public partial class Block8x8FTests : JpegFixture
RunTest, RunTest,
srcSeed, srcSeed,
qtSeed, qtSeed,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableSSE); HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
} }
[Fact] [Fact]
@ -462,7 +462,7 @@ public partial class Block8x8FTests : JpegFixture
// 3. DisableAvx2 - call fallback code of float implementation // 3. DisableAvx2 - call fallback code of float implementation
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest, RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableSSE); HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
} }
[Theory] [Theory]

13
tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs

@ -152,7 +152,7 @@ public static class DCTTests
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest, RunTest,
seed, seed,
HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
} }
[Theory] [Theory]
@ -352,15 +352,14 @@ public static class DCTTests
Assert.Equal(expectedDest, actualDest, new ApproximateFloatComparer(1f)); Assert.Equal(expectedDest, actualDest, new ApproximateFloatComparer(1f));
} }
// 4 paths: // 3 paths:
// 1. AllowAll - call avx/fma implementation // 1. AllowAll - call avx implementation
// 2. DisableFMA - call avx without fma implementation // 2. DisableAvx - call Vector4 implementation
// 3. DisableAvx - call Vector4 implementation // 3. DisableHWIntrinsic - call scalar fallback implementation
// 4. DisableHWIntrinsic - call scalar fallback implementation
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest, RunTest,
seed, seed,
HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
} }
} }
} }

14
tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs

@ -19,12 +19,8 @@ public class JpegColorConverterTests
private const int TestBufferLength = 40; private const int TestBufferLength = 40;
private const HwIntrinsics IntrinsicsConfig = HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX2;
private static readonly ApproximateColorProfileComparer ColorSpaceComparer = new(epsilon: Precision); private static readonly ApproximateColorProfileComparer ColorSpaceComparer = new(epsilon: Precision);
private static readonly ColorProfileConverter ColorSpaceConverter = new();
public static readonly TheoryData<int> Seeds = new() { 1, 2, 3 }; public static readonly TheoryData<int> Seeds = new() { 1, 2, 3 };
public JpegColorConverterTests(ITestOutputHelper output) public JpegColorConverterTests(ITestOutputHelper output)
@ -73,7 +69,7 @@ public class JpegColorConverterTests
{ {
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest, RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSE2 | HwIntrinsics.DisableHWIntrinsic); HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
static void RunTest(string arg) static void RunTest(string arg)
{ {
@ -106,7 +102,7 @@ public class JpegColorConverterTests
{ {
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest, RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSE2 | HwIntrinsics.DisableHWIntrinsic); HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic);
static void RunTest(string arg) static void RunTest(string arg)
{ {
@ -139,7 +135,7 @@ public class JpegColorConverterTests
{ {
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest, RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSE2 | HwIntrinsics.DisableHWIntrinsic); HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic);
static void RunTest(string arg) static void RunTest(string arg)
{ {
@ -172,7 +168,7 @@ public class JpegColorConverterTests
{ {
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest, RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSE2 | HwIntrinsics.DisableHWIntrinsic); HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic);
static void RunTest(string arg) static void RunTest(string arg)
{ {
@ -205,7 +201,7 @@ public class JpegColorConverterTests
{ {
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest, RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSE2 | HwIntrinsics.DisableHWIntrinsic); HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic);
static void RunTest(string arg) static void RunTest(string arg)
{ {

16
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs

@ -417,4 +417,20 @@ public partial class JpegDecoderTests
image.DebugSave(provider); image.DebugSave(provider);
image.CompareToReferenceOutput(provider); image.CompareToReferenceOutput(provider);
} }
// https://github.com/SixLabors/ImageSharp/issues/2948
[Theory]
[WithFile(TestImages.Jpeg.Issues.Issue2948, PixelTypes.Rgb24)]
public void Issue2948_No_SOS_Decode_Throws_InvalidImageContentException<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
=> Assert.Throws<InvalidImageContentException>(() =>
{
using Image<TPixel> image = provider.GetImage(JpegDecoder.Instance);
});
// https://github.com/SixLabors/ImageSharp/issues/2948
[Theory]
[InlineData(TestImages.Jpeg.Issues.Issue2948)]
public void Issue2948_No_SOS_Identify_Throws_InvalidImageContentException(string imagePath)
=> Assert.Throws<InvalidImageContentException>(() => _ = Image.Identify(TestFile.Create(imagePath).Bytes));
} }

2
tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs

@ -171,7 +171,7 @@ public class PngDecoderFilterTests
public void PaethFilter_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPaethFilterTest, HwIntrinsics.AllowAll); public void PaethFilter_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPaethFilterTest, HwIntrinsics.AllowAll);
[Fact] [Fact]
public void PaethFilter_WithoutSsse3_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPaethFilterTest, HwIntrinsics.DisableSSSE3); public void PaethFilter_WithoutSsse3_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPaethFilterTest, HwIntrinsics.DisableHWIntrinsic);
[Fact] [Fact]
public void PaethFilter_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPaethFilterTest, HwIntrinsics.DisableHWIntrinsic); public void PaethFilter_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPaethFilterTest, HwIntrinsics.DisableHWIntrinsic);

2
tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.cs

@ -51,7 +51,7 @@ public class PngEncoderFilterTests : MeasureFixture
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest, RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSSE3); HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic);
} }
[Fact] [Fact]

2
tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

@ -618,7 +618,7 @@ public partial class PngEncoderTests
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest, RunTest,
HwIntrinsics.DisableSSSE3, HwIntrinsics.DisableHWIntrinsic,
provider); provider);
} }

6
tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs

@ -360,7 +360,9 @@ public class TiffDecoderTests : TiffDecoderBaseTester
{ {
using Image<TPixel> image = provider.GetImage(TiffDecoder.Instance); using Image<TPixel> image = provider.GetImage(TiffDecoder.Instance);
image.DebugSave(provider); image.DebugSave(provider);
image.CompareToReferenceOutput(ImageComparer.Exact, provider);
// ARM reports a 0.0000% difference, so we use a tolerant comparer here.
image.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.0001F), provider);
} }
[Theory] [Theory]
@ -783,7 +785,7 @@ public class TiffDecoderTests : TiffDecoderBaseTester
// ImageMagick cannot decode this image. // ImageMagick cannot decode this image.
image.DebugSave(provider); image.DebugSave(provider);
image.CompareToReferenceOutput( image.CompareToReferenceOutput(
ImageComparer.TolerantPercentage(0.0018F), // NET 9+ Uses zlib-ng to decompress, which manages to decode 2 extra pixels. ImageComparer.TolerantPercentage(0.0034F), // NET 10 Uses zlib-ng to decompress, which manages to decode 3 extra pixels.
provider, provider,
appendPixelTypeToFileName: false); appendPixelTypeToFileName: false);
} }

4
tests/ImageSharp.Tests/Formats/WebP/ColorSpaceTransformUtilsTests.cs

@ -71,7 +71,7 @@ public class ColorSpaceTransformUtilsTests
public void CollectColorBlueTransforms_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCollectColorBlueTransformsTest, HwIntrinsics.AllowAll); public void CollectColorBlueTransforms_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCollectColorBlueTransformsTest, HwIntrinsics.AllowAll);
[Fact] [Fact]
public void CollectColorBlueTransforms_WithoutVector128_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCollectColorBlueTransformsTest, HwIntrinsics.DisableSSE41); public void CollectColorBlueTransforms_WithoutVector128_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCollectColorBlueTransformsTest, HwIntrinsics.DisableHWIntrinsic);
[Fact] [Fact]
public void CollectColorBlueTransforms_WithoutVector256_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCollectColorBlueTransformsTest, HwIntrinsics.DisableAVX2); public void CollectColorBlueTransforms_WithoutVector256_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCollectColorBlueTransformsTest, HwIntrinsics.DisableAVX2);
@ -80,7 +80,7 @@ public class ColorSpaceTransformUtilsTests
public void CollectColorRedTransforms_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCollectColorRedTransformsTest, HwIntrinsics.AllowAll); public void CollectColorRedTransforms_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCollectColorRedTransformsTest, HwIntrinsics.AllowAll);
[Fact] [Fact]
public void CollectColorRedTransforms_WithoutVector128_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCollectColorRedTransformsTest, HwIntrinsics.DisableSSE41); public void CollectColorRedTransforms_WithoutVector128_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCollectColorRedTransformsTest, HwIntrinsics.DisableHWIntrinsic);
[Fact] [Fact]
public void CollectColorRedTransforms_WithoutVector256_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCollectColorRedTransformsTest, HwIntrinsics.DisableAVX2); public void CollectColorRedTransforms_WithoutVector256_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCollectColorRedTransformsTest, HwIntrinsics.DisableAVX2);

14
tests/ImageSharp.Tests/Formats/WebP/LosslessUtilsTests.cs

@ -304,19 +304,19 @@ public class LosslessUtilsTests
public void Predictor11_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPredictor11Test, HwIntrinsics.AllowAll); public void Predictor11_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPredictor11Test, HwIntrinsics.AllowAll);
[Fact] [Fact]
public void Predictor11_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPredictor11Test, HwIntrinsics.DisableSSE2); public void Predictor11_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPredictor11Test, HwIntrinsics.DisableHWIntrinsic);
[Fact] [Fact]
public void Predictor12_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPredictor12Test, HwIntrinsics.AllowAll); public void Predictor12_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPredictor12Test, HwIntrinsics.AllowAll);
[Fact] [Fact]
public void Predictor12_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPredictor12Test, HwIntrinsics.DisableSSE2); public void Predictor12_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPredictor12Test, HwIntrinsics.DisableHWIntrinsic);
[Fact] [Fact]
public void Predictor13_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPredictor13Test, HwIntrinsics.AllowAll); public void Predictor13_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPredictor13Test, HwIntrinsics.AllowAll);
[Fact] [Fact]
public void Predictor13_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPredictor13Test, HwIntrinsics.DisableSSE2); public void Predictor13_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPredictor13Test, HwIntrinsics.DisableHWIntrinsic);
[Fact] [Fact]
public void SubtractGreen_Works() => RunSubtractGreenTest(); public void SubtractGreen_Works() => RunSubtractGreenTest();
@ -331,7 +331,7 @@ public class LosslessUtilsTests
public void SubtractGreen_Scalar_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSubtractGreenTest, HwIntrinsics.DisableHWIntrinsic); public void SubtractGreen_Scalar_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSubtractGreenTest, HwIntrinsics.DisableHWIntrinsic);
[Fact] [Fact]
public void SubtractGreen_WithoutAvxOrSSSE3_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSubtractGreenTest, HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSSE3); public void SubtractGreen_WithoutAvxOrSSSE3_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSubtractGreenTest, HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic);
[Fact] [Fact]
public void AddGreenToBlueAndRed_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAddGreenToBlueAndRedTest, HwIntrinsics.AllowAll); public void AddGreenToBlueAndRed_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAddGreenToBlueAndRedTest, HwIntrinsics.AllowAll);
@ -340,13 +340,13 @@ public class LosslessUtilsTests
public void AddGreenToBlueAndRed_WithoutAVX2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAddGreenToBlueAndRedTest, HwIntrinsics.DisableAVX2); public void AddGreenToBlueAndRed_WithoutAVX2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAddGreenToBlueAndRedTest, HwIntrinsics.DisableAVX2);
[Fact] [Fact]
public void AddGreenToBlueAndRed_WithoutAVX2OrSSSE3_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAddGreenToBlueAndRedTest, HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSE2 | HwIntrinsics.DisableSSSE3); public void AddGreenToBlueAndRed_WithoutAVX2OrSSSE3_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAddGreenToBlueAndRedTest, HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic);
[Fact] [Fact]
public void TransformColor_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTransformColorTest, HwIntrinsics.AllowAll); public void TransformColor_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTransformColorTest, HwIntrinsics.AllowAll);
[Fact] [Fact]
public void TransformColor_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTransformColorTest, HwIntrinsics.DisableSSE2); public void TransformColor_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTransformColorTest, HwIntrinsics.DisableHWIntrinsic);
[Fact] [Fact]
public void TransformColor_WithoutAVX2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTransformColorTest, HwIntrinsics.DisableAVX2); public void TransformColor_WithoutAVX2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTransformColorTest, HwIntrinsics.DisableAVX2);
@ -355,7 +355,7 @@ public class LosslessUtilsTests
public void TransformColorInverse_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTransformColorInverseTest, HwIntrinsics.AllowAll); public void TransformColorInverse_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTransformColorInverseTest, HwIntrinsics.AllowAll);
[Fact] [Fact]
public void TransformColorInverse_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTransformColorInverseTest, HwIntrinsics.DisableSSE2); public void TransformColorInverse_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTransformColorInverseTest, HwIntrinsics.DisableHWIntrinsic);
[Fact] [Fact]
public void TransformColorInverse_WithoutAVX2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTransformColorInverseTest, HwIntrinsics.DisableAVX2); public void TransformColorInverse_WithoutAVX2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTransformColorInverseTest, HwIntrinsics.DisableAVX2);

4
tests/ImageSharp.Tests/Formats/WebP/PredictorEncoderTests.cs

@ -24,7 +24,7 @@ public class PredictorEncoderTests
[Fact] [Fact]
public void ColorSpaceTransform_WithPeakImage_WithoutSSE41_Works() public void ColorSpaceTransform_WithPeakImage_WithoutSSE41_Works()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ColorSpaceTransform_WithPeakImage_ProducesExpectedData, HwIntrinsics.DisableSSE41); => FeatureTestRunner.RunWithHwIntrinsicsFeature(ColorSpaceTransform_WithPeakImage_ProducesExpectedData, HwIntrinsics.DisableHWIntrinsic);
[Fact] [Fact]
public void ColorSpaceTransform_WithBikeImage_WithHardwareIntrinsics_Works() public void ColorSpaceTransform_WithBikeImage_WithHardwareIntrinsics_Works()
@ -32,7 +32,7 @@ public class PredictorEncoderTests
[Fact] [Fact]
public void ColorSpaceTransform_WithBikeImage_WithoutSSE41_Works() public void ColorSpaceTransform_WithBikeImage_WithoutSSE41_Works()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ColorSpaceTransform_WithBikeImage_ProducesExpectedData, HwIntrinsics.DisableSSE41); => FeatureTestRunner.RunWithHwIntrinsicsFeature(ColorSpaceTransform_WithBikeImage_ProducesExpectedData, HwIntrinsics.DisableHWIntrinsic);
[Fact] [Fact]
public void ColorSpaceTransform_WithBikeImage_WithoutAvx2_Works() public void ColorSpaceTransform_WithBikeImage_WithoutAvx2_Works()

2
tests/ImageSharp.Tests/Formats/WebP/QuantEncTests.cs

@ -45,7 +45,7 @@ public class QuantEncTests
public void QuantizeBlock_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunQuantizeBlockTest, HwIntrinsics.AllowAll); public void QuantizeBlock_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunQuantizeBlockTest, HwIntrinsics.AllowAll);
[Fact] [Fact]
public void QuantizeBlock_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunQuantizeBlockTest, HwIntrinsics.DisableSSE2); public void QuantizeBlock_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunQuantizeBlockTest, HwIntrinsics.DisableHWIntrinsic);
[Fact] [Fact]
public void QuantizeBlock_WithoutAVX2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunQuantizeBlockTest, HwIntrinsics.DisableAVX2); public void QuantizeBlock_WithoutAVX2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunQuantizeBlockTest, HwIntrinsics.DisableAVX2);

2
tests/ImageSharp.Tests/Formats/WebP/Vp8ResidualTests.cs

@ -252,5 +252,5 @@ public class Vp8ResidualTests
public void SetCoeffsTest_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSetCoeffsTest, HwIntrinsics.AllowAll); public void SetCoeffsTest_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSetCoeffsTest, HwIntrinsics.AllowAll);
[Fact] [Fact]
public void SetCoeffsTest_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSetCoeffsTest, HwIntrinsics.DisableSSE2); public void SetCoeffsTest_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSetCoeffsTest, HwIntrinsics.DisableHWIntrinsic);
} }

4
tests/ImageSharp.Tests/Formats/WebP/WebpCommonUtilsTests.cs

@ -23,7 +23,7 @@ public class WebpCommonUtilsTests
[Fact] [Fact]
public void CheckNonOpaque_WithOpaquePixels_WithoutSse2_Works() public void CheckNonOpaque_WithOpaquePixels_WithoutSse2_Works()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCheckNoneOpaqueWithOpaquePixelsTest, HwIntrinsics.DisableSSE2); => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCheckNoneOpaqueWithOpaquePixelsTest, HwIntrinsics.DisableHWIntrinsic);
[Fact] [Fact]
public void CheckNonOpaque_WithOpaquePixels_WithoutAvx2_Works() public void CheckNonOpaque_WithOpaquePixels_WithoutAvx2_Works()
@ -35,7 +35,7 @@ public class WebpCommonUtilsTests
[Fact] [Fact]
public void CheckNonOpaque_WithNoneOpaquePixels_WithoutSse2_Works() public void CheckNonOpaque_WithNoneOpaquePixels_WithoutSse2_Works()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCheckNoneOpaqueWithNoneOpaquePixelsTest, HwIntrinsics.DisableSSE2); => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCheckNoneOpaqueWithNoneOpaquePixelsTest, HwIntrinsics.DisableHWIntrinsic);
[Fact] [Fact]
public void CheckNonOpaque_WithNoneOpaquePixels_WithoutAvx2_Works() public void CheckNonOpaque_WithNoneOpaquePixels_WithoutAvx2_Works()

2
tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs

@ -32,7 +32,7 @@ public class YuvConversionTests
public void UpSampleYuvToRgb_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunUpSampleYuvToRgbTest, HwIntrinsics.AllowAll); public void UpSampleYuvToRgb_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunUpSampleYuvToRgbTest, HwIntrinsics.AllowAll);
[Fact] [Fact]
public void UpSampleYuvToRgb_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunUpSampleYuvToRgbTest, HwIntrinsics.DisableSSE2); public void UpSampleYuvToRgb_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunUpSampleYuvToRgbTest, HwIntrinsics.DisableHWIntrinsic);
[Theory] [Theory]
[WithFile(TestImages.Webp.Yuv, PixelTypes.Rgba32)] [WithFile(TestImages.Webp.Yuv, PixelTypes.Rgba32)]

4
tests/ImageSharp.Tests/Image/ImageTests.DetectFormat.cs

@ -50,6 +50,10 @@ public partial class ImageTests
Assert.Equal(this.LocalImageFormat, format); Assert.Equal(this.LocalImageFormat, format);
} }
[Fact]
public void FromBytes_EmptySpan_Throws()
=> Assert.Throws<UnknownImageFormatException>(() => Image.DetectFormat([]));
[Fact] [Fact]
public void FromFileSystemPath_GlobalConfiguration() public void FromFileSystemPath_GlobalConfiguration()
{ {

4
tests/ImageSharp.Tests/Image/ImageTests.Identify.cs

@ -38,6 +38,10 @@ public partial class ImageTests
Assert.Equal(ExpectedGlobalFormat, info.Metadata.DecodedImageFormat); Assert.Equal(ExpectedGlobalFormat, info.Metadata.DecodedImageFormat);
} }
[Fact]
public void FromBytes_EmptySpan_Throws()
=> Assert.Throws<UnknownImageFormatException>(() => Image.Identify([]));
[Fact] [Fact]
public void FromBytes_CustomConfiguration() public void FromBytes_CustomConfiguration()
{ {

11
tests/ImageSharp.Tests/Image/ImageTests.Load_FromBytes_PassLocalConfiguration.cs

@ -79,5 +79,16 @@ public partial class ImageTests
this.TestFormat.VerifyAgnosticDecodeCall(this.Marker, this.TopLevelConfiguration); this.TestFormat.VerifyAgnosticDecodeCall(this.Marker, this.TopLevelConfiguration);
} }
[Fact]
public void FromBytes_EmptySpan_Throws()
{
DecoderOptions options = new()
{
Configuration = this.TopLevelConfiguration
};
Assert.Throws<UnknownImageFormatException>(() => Image.Load(options, []));
}
} }
} }

4
tests/ImageSharp.Tests/Image/ImageTests.Load_FromBytes_UseGlobalConfiguration.cs

@ -45,5 +45,9 @@ public partial class ImageTests
VerifyDecodedImage(img); VerifyDecodedImage(img);
Assert.IsType<BmpFormat>(img.Metadata.DecodedImageFormat); Assert.IsType<BmpFormat>(img.Metadata.DecodedImageFormat);
} }
[Fact]
public void FromBytes_EmptySpan_Throws()
=> Assert.ThrowsAny<UnknownImageFormatException>(() => Image.Load<Rgba32>([]));
} }
} }

13
tests/ImageSharp.Tests/Image/ImageTests.Load_FromStream_ThrowsRightException.cs

@ -32,6 +32,17 @@ public partial class ImageTests
} }
}); });
public void Dispose() => this.Stream?.Dispose(); [Fact]
public void FromStream_Empty_Throws()
{
using MemoryStream ms = new();
Assert.Throws<UnknownImageFormatException>(() => Image.Load(DecoderOptions.Default, ms));
}
public void Dispose()
{
this.Stream?.Dispose();
GC.SuppressFinalize(this);
}
} }
} }

2
tests/ImageSharp.Tests/ImageSharp.Tests.csproj

@ -12,7 +12,7 @@
<Choose> <Choose>
<When Condition="$(SIXLABORS_TESTING_PREVIEW) == true"> <When Condition="$(SIXLABORS_TESTING_PREVIEW) == true">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks> <TargetFrameworks>net8.0;net10.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
</When> </When>
<Otherwise> <Otherwise>

4
tests/ImageSharp.Tests/Metadata/Profiles/ICC/DataWriter/IccDataWriterPrimitivesTests.cs

@ -42,7 +42,7 @@ public class IccDataWriterPrimitivesTests
byte[] output = writer.GetData(); byte[] output = writer.GetData();
Assert.Equal(0, count); Assert.Equal(0, count);
Assert.Equal([], output); Assert.Equal(Array.Empty<byte>(), output);
} }
[Fact] [Fact]
@ -62,7 +62,7 @@ public class IccDataWriterPrimitivesTests
byte[] output = writer.GetData(); byte[] output = writer.GetData();
Assert.Equal(0, count); Assert.Equal(0, count);
Assert.Equal([], output); Assert.Equal(Array.Empty<byte>(), output);
} }
[Theory] [Theory]

2
tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs

@ -148,7 +148,7 @@ public class BokehBlurTest
[Theory] [Theory]
[WithFileCollection(nameof(TestFiles), nameof(BokehBlurValues), PixelTypes.Rgba32, HwIntrinsics.AllowAll)] [WithFileCollection(nameof(TestFiles), nameof(BokehBlurValues), PixelTypes.Rgba32, HwIntrinsics.AllowAll)]
[WithFileCollection(nameof(TestFiles), nameof(BokehBlurValues), PixelTypes.Rgba32, HwIntrinsics.DisableSSE41)] [WithFileCollection(nameof(TestFiles), nameof(BokehBlurValues), PixelTypes.Rgba32, HwIntrinsics.DisableHWIntrinsic)]
public void BokehBlurFilterProcessor_Bounded(TestImageProvider<Rgba32> provider, BokehBlurInfo value, HwIntrinsics intrinsicsFilter) public void BokehBlurFilterProcessor_Bounded(TestImageProvider<Rgba32> provider, BokehBlurInfo value, HwIntrinsics intrinsicsFilter)
{ {
static void RunTest(string arg1, string arg2) static void RunTest(string arg1, string arg2)

40
tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs

@ -234,7 +234,23 @@ public class AffineTransformTests
image.DebugSave(provider); image.DebugSave(provider);
Assert.Equal(4, image.Width); Assert.Equal(4, image.Width);
Assert.Equal(8, image.Height); Assert.Equal(7, image.Height);
}
[Theory]
[WithFile(TestImages.Png.Issue3000, PixelTypes.Rgba32, 3, 3)]
[WithFile(TestImages.Png.Issue3000, PixelTypes.Rgba32, 4, 4)]
public void Issue3000<TPixel>(TestImageProvider<TPixel> provider, float x, float y)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
image.Mutate(c => c
.Transform(new AffineTransformBuilder().AppendRotationDegrees(90, new Vector2(x, y))));
string details = $"p-{x}-{y}";
image.DebugSave(provider, testOutputDetails: details);
image.CompareToReferenceOutput(ValidatorComparer, provider, testOutputDetails: details);
} }
[Theory] [Theory]
@ -267,31 +283,41 @@ public class AffineTransformTests
image.CompareToReferenceOutput(ValidatorComparer, provider, testOutputDetails: radians); image.CompareToReferenceOutput(ValidatorComparer, provider, testOutputDetails: radians);
} }
[Fact] [Theory]
public void TransformRotationDoesNotOffset() [WithSolidFilledImages(100, 100, "DimGray", PixelTypes.Rgba32)]
public void TransformRotationDoesNotOffset<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{ {
Rgba32 background = Color.DimGray.ToPixel<Rgba32>(); Rgba32 background = Color.DimGray.ToPixel<Rgba32>();
Rgba32 marker = Color.Aqua.ToPixel<Rgba32>(); TPixel marker = Color.Aqua.ToPixel<TPixel>();
using Image<TPixel> canvas = provider.GetImage();
using Image<Rgba32> img = new(100, 100, background); using Image<TPixel> img = canvas.Clone();
img[0, 0] = marker; img[0, 0] = marker;
img.Mutate(c => c.Rotate(180)); img.Mutate(c => c.Rotate(180));
Assert.Equal(marker, img[99, 99]); Assert.Equal(marker, img[99, 99]);
using Image<Rgba32> img2 = new(100, 100, background); img.DebugSave(provider, "Rotate180");
using Image<TPixel> img2 = canvas.Clone();
img2[0, 0] = marker; img2[0, 0] = marker;
img2.Mutate( img2.Mutate(
c => c =>
c.Transform(new AffineTransformBuilder().AppendRotationDegrees(180), KnownResamplers.NearestNeighbor)); c.Transform(new AffineTransformBuilder().AppendRotationDegrees(180), KnownResamplers.NearestNeighbor));
using Image<Rgba32> img3 = new(100, 100, background); img.DebugSave(provider, "AffineRotate180NN");
using Image<TPixel> img3 = canvas.Clone();
img3[0, 0] = marker; img3[0, 0] = marker;
img3.Mutate(c => c.Transform(new AffineTransformBuilder().AppendRotationDegrees(180))); img3.Mutate(c => c.Transform(new AffineTransformBuilder().AppendRotationDegrees(180)));
img3.DebugSave(provider, "AffineRotate180Bicubic");
ImageComparer.Exact.VerifySimilarity(img, img2); ImageComparer.Exact.VerifySimilarity(img, img2);
ImageComparer.Exact.VerifySimilarity(img, img3); ImageComparer.Exact.VerifySimilarity(img, img3);
} }

8
tests/ImageSharp.Tests/Processing/Processors/Transforms/ProjectiveTransformTests.cs

@ -249,17 +249,17 @@ public class ProjectiveTransformTests
image.Mutate(ctx => ctx.Transform(builder)); image.Mutate(ctx => ctx.Transform(builder));
// A 180-degree rotation inverts both axes around the image center. // A 180-degree rotation inverts both axes around the image center.
// The subject location (5, 15) becomes (imageWidth - 5 - 1, imageHeight - 15 - 1) = (94, 84) // The subject location (5, 15) becomes (imageWidth - 5, imageHeight - 15) = (95, 85)
Assert.Equal( Assert.Equal(
[94, 84], [95, 85],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectLocation).Value); image.Metadata.ExifProfile.GetValue(ExifTag.SubjectLocation).Value);
// The subject area is also mirrored around the center. // The subject area is also mirrored around the center.
// New X = imageWidth - originalX - width // New X = imageWidth - originalX - width
// New Y = imageHeight - originalY - height // New Y = imageHeight - originalY - height
// (5, 15, 50, 50) becomes (44, 34, 50, 50) // (5, 15, 50, 50) becomes (45, 35, 50, 50)
Assert.Equal( Assert.Equal(
[44, 34, 50, 50], [45, 35, 50, 50],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectArea).Value); image.Metadata.ExifProfile.GetValue(ExifTag.SubjectArea).Value);
} }

2
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs

@ -652,7 +652,7 @@ public class ResizeTests
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectLocation).Value); image.Metadata.ExifProfile.GetValue(ExifTag.SubjectLocation).Value);
Assert.Equal( Assert.Equal(
[2, 7, 11, 11], [2, 7, 10, 10],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectArea).Value); image.Metadata.ExifProfile.GetValue(ExifTag.SubjectArea).Value);
} }
} }

8
tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs

@ -55,17 +55,17 @@ public class RotateTests
image.Mutate(ctx => ctx.Rotate(180)); image.Mutate(ctx => ctx.Rotate(180));
// A 180-degree rotation inverts both axes around the image center. // A 180-degree rotation inverts both axes around the image center.
// The subject location (5, 15) becomes (imageWidth - 5 - 1, imageHeight - 15 - 1) = (94, 84) // The subject location (5, 15) becomes (imageWidth - 5, imageHeight - 15) = (95, 85)
Assert.Equal( Assert.Equal(
[94, 84], [95, 85],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectLocation).Value); image.Metadata.ExifProfile.GetValue(ExifTag.SubjectLocation).Value);
// The subject area is also mirrored around the center. // The subject area is also mirrored around the center.
// New X = imageWidth - originalX - width // New X = imageWidth - originalX - width
// New Y = imageHeight - originalY - height // New Y = imageHeight - originalY - height
// (5, 15, 50, 50) becomes (44, 34, 50, 50) // (5, 15, 50, 50) becomes (45, 35, 50, 50)
Assert.Equal( Assert.Equal(
[44, 34, 50, 50], [45, 35, 50, 50],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectArea).Value); image.Metadata.ExifProfile.GetValue(ExifTag.SubjectArea).Value);
} }
} }

2
tests/ImageSharp.Tests/Processing/Transforms/ResizeTests.cs

@ -102,6 +102,6 @@ public class ResizeTests : BaseImageOperationsExtensionTest
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest, RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableFMA); HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2);
} }
} }

4
tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs

@ -98,7 +98,7 @@ public abstract class TransformBuilderTestBase<TBuilder>
this.AppendRotationDegrees(builder, degrees); this.AppendRotationDegrees(builder, degrees);
// TODO: We should also test CreateRotationMatrixDegrees() (and all TransformUtils stuff!) for correctness // TODO: We should also test CreateRotationMatrixDegrees() (and all TransformUtils stuff!) for correctness
Matrix3x2 matrix = TransformUtils.CreateRotationTransformMatrixDegrees(degrees, size, TransformSpace.Pixel); Matrix3x2 matrix = TransformUtilities.CreateRotationTransformMatrixDegrees(degrees, size);
Vector2 position = new(x, y); Vector2 position = new(x, y);
Vector2 expected = Vector2.Transform(position, matrix); Vector2 expected = Vector2.Transform(position, matrix);
@ -152,7 +152,7 @@ public abstract class TransformBuilderTestBase<TBuilder>
this.AppendSkewDegrees(builder, degreesX, degreesY); this.AppendSkewDegrees(builder, degreesX, degreesY);
Matrix3x2 matrix = TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size, TransformSpace.Pixel); Matrix3x2 matrix = TransformUtilities.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size);
Vector2 position = new(x, y); Vector2 position = new(x, y);
Vector2 expected = Vector2.Transform(position, matrix); Vector2 expected = Vector2.Transform(position, matrix);

4
tests/ImageSharp.Tests/TestImages.cs

@ -163,6 +163,9 @@ public static class TestImages
// Issue 2924: https://github.com/SixLabors/ImageSharp/issues/2924 // Issue 2924: https://github.com/SixLabors/ImageSharp/issues/2924
public const string Issue2924 = "Png/issues/Issue_2924.png"; public const string Issue2924 = "Png/issues/Issue_2924.png";
// Issue 3000: https://github.com/SixLabors/ImageSharp/issues/3000
public const string Issue3000 = "Png/issues/issue_3000.png";
public static class Bad public static class Bad
{ {
public const string MissingDataChunk = "Png/xdtn0g01.png"; public const string MissingDataChunk = "Png/xdtn0g01.png";
@ -345,6 +348,7 @@ public static class TestImages
public const string Issue2638 = "Jpg/issues/Issue2638.jpg"; public const string Issue2638 = "Jpg/issues/Issue2638.jpg";
public const string Issue2758 = "Jpg/issues/issue-2758.jpg"; public const string Issue2758 = "Jpg/issues/issue-2758.jpg";
public const string Issue2857 = "Jpg/issues/issue-2857-subsub-ifds.jpg"; public const string Issue2857 = "Jpg/issues/issue-2857-subsub-ifds.jpg";
public const string Issue2948 = "Jpg/issues/issue-2948-sos.jpg";
public static class Fuzz public static class Fuzz
{ {

65
tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs

@ -436,37 +436,38 @@ public enum HwIntrinsics : long
// Use flags so we can pass multiple values without using params. // Use flags so we can pass multiple values without using params.
// Don't base on 0 or use inverse for All as that doesn't translate to string values. // Don't base on 0 or use inverse for All as that doesn't translate to string values.
DisableHWIntrinsic = 1L << 0, DisableHWIntrinsic = 1L << 0,
DisableSSE = 1L << 1, DisableSSE42 = 1L << 1,
DisableSSE2 = 1L << 2, DisableAVX = 1L << 2,
DisableAES = 1L << 3, DisableAVX2 = 1L << 3,
DisablePCLMULQDQ = 1L << 4, DisableAVX512 = 1L << 4,
DisableSSE3 = 1L << 5, DisableAVX512v2 = 1L << 5,
DisableSSSE3 = 1L << 6, DisableAVX512v3 = 1L << 6,
DisableSSE41 = 1L << 7, DisableAVX10v1 = 1L << 7,
DisableSSE42 = 1L << 8, DisableAVX10v2 = 1L << 8,
DisablePOPCNT = 1L << 9, DisableAPX = 1L << 9,
DisableAVX = 1L << 10, DisableAES = 1L << 10,
DisableFMA = 1L << 11, DisableAVX512VP2INTERSECT = 1L << 11,
DisableAVX2 = 1L << 12, DisableAVXIFMA = 1L << 12,
DisableAVXVNNI = 1L << 13, DisableAVXVNNI = 1L << 13,
DisableAVX512BW = 1L << 14, DisableAVXVNNIINT = 1L << 14,
DisableAVX512BW_VL = 1L << 15, DisableGFNI = 1L << 15,
DisableAVX512CD = 1L << 16, DisableSHA = 1L << 16,
DisableAVX512CD_VL = 1L << 17, DisableVAES = 1L << 17,
DisableAVX512DQ = 1L << 18, DisableWAITPKG = 1L << 18,
DisableAVX512DQ_VL = 1L << 19, DisableX86Serialize = 1 << 19,
DisableAVX512F = 1L << 20, // Arm64
DisableAVX512F_VL = 1L << 21, DisableArm64Aes = 1L << 20,
DisableAVX512VBMI = 1L << 22, DisableArm64Atomics = 1L << 21,
DisableAVX512VBMI_VL = 1L << 23, DisableArm64Crc32 = 1L << 22,
DisableBMI1 = 1L << 24, DisableArm64Dczva = 1L << 23,
DisableBMI2 = 1L << 25, DisableArm64Dp = 1L << 24,
DisableLZCNT = 1L << 26, DisableArm64Rdm = 1L << 25,
DisableArm64AdvSimd = 1L << 27, DisableArm64Sha1 = 1L << 26,
DisableArm64Crc32 = 1L << 28, DisableArm64Sha256 = 1L << 27,
DisableArm64Dp = 1L << 29, DisableArm64Sve = 1L << 28,
DisableArm64Aes = 1L << 30, DisableArm64Sve2 = 1L << 29,
DisableArm64Sha1 = 1L << 31, // RISC-V64
DisableArm64Sha256 = 1L << 32, DisableRiscV64Zba = 1L << 30,
AllowAll = 1L << 33 DisableRiscV64Zbb = 1L << 31,
AllowAll = 1L << 32,
} }

259
tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs

@ -47,7 +47,7 @@ public class FeatureTestRunnerTests
} }
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
() => Assert.True(Vector.IsHardwareAccelerated), () => Assert.True(Vector.IsHardwareAccelerated, "Vector hardware acceleration should be enabled when AllowAll is specified."),
HwIntrinsics.AllowAll); HwIntrinsics.AllowAll);
} }
@ -56,21 +56,21 @@ public class FeatureTestRunnerTests
{ {
static void AssertDisabled() static void AssertDisabled()
{ {
Assert.False(Sse.IsSupported); Assert.False(Sse.IsSupported, "SSE should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse2.IsSupported); Assert.False(Sse2.IsSupported, "SSE2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Aes.IsSupported); Assert.False(Aes.IsSupported, "AES (x86) should be disabled when DisableHWIntrinsic is set.");
Assert.False(Pclmulqdq.IsSupported); Assert.False(Pclmulqdq.IsSupported, "PCLMULQDQ should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse3.IsSupported); Assert.False(Sse3.IsSupported, "SSE3 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Ssse3.IsSupported); Assert.False(Ssse3.IsSupported, "SSSE3 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse41.IsSupported); Assert.False(Sse41.IsSupported, "SSE4.1 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse42.IsSupported); Assert.False(Sse42.IsSupported, "SSE4.2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Popcnt.IsSupported); Assert.False(Popcnt.IsSupported, "POPCNT should be disabled when DisableHWIntrinsic is set.");
Assert.False(Avx.IsSupported); Assert.False(Avx.IsSupported, "AVX should be disabled when DisableHWIntrinsic is set.");
Assert.False(Fma.IsSupported); Assert.False(Fma.IsSupported, "FMA should be disabled when DisableHWIntrinsic is set.");
Assert.False(Avx2.IsSupported); Assert.False(Avx2.IsSupported, "AVX2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Bmi1.IsSupported); Assert.False(Bmi1.IsSupported, "BMI1 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Bmi2.IsSupported); Assert.False(Bmi2.IsSupported, "BMI2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Lzcnt.IsSupported); Assert.False(Lzcnt.IsSupported, "LZCNT should be disabled when DisableHWIntrinsic is set.");
} }
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
@ -88,90 +88,70 @@ public class FeatureTestRunnerTests
switch (Enum.Parse<HwIntrinsics>(intrinsic)) switch (Enum.Parse<HwIntrinsics>(intrinsic))
{ {
case HwIntrinsics.DisableHWIntrinsic: case HwIntrinsics.DisableHWIntrinsic:
Assert.False(Sse.IsSupported); Assert.False(Sse.IsSupported, "SSE should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse2.IsSupported); Assert.False(Sse2.IsSupported, "SSE2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Aes.IsSupported); Assert.False(Aes.IsSupported, "AES (x86) should be disabled when DisableHWIntrinsic is set.");
Assert.False(Pclmulqdq.IsSupported); Assert.False(Pclmulqdq.IsSupported, "PCLMULQDQ should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse3.IsSupported); Assert.False(Sse3.IsSupported, "SSE3 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Ssse3.IsSupported); Assert.False(Ssse3.IsSupported, "SSSE3 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse41.IsSupported); Assert.False(Sse41.IsSupported, "SSE4.1 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse42.IsSupported); Assert.False(Sse42.IsSupported, "SSE4.2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Popcnt.IsSupported); Assert.False(Popcnt.IsSupported, "POPCNT should be disabled when DisableHWIntrinsic is set.");
Assert.False(Avx.IsSupported); Assert.False(Avx.IsSupported, "AVX should be disabled when DisableHWIntrinsic is set.");
Assert.False(Fma.IsSupported); Assert.False(Fma.IsSupported, "FMA should be disabled when DisableHWIntrinsic is set.");
Assert.False(Avx2.IsSupported); Assert.False(Avx2.IsSupported, "AVX2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Bmi1.IsSupported); Assert.False(Bmi1.IsSupported, "BMI1 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Bmi2.IsSupported); Assert.False(Bmi2.IsSupported, "BMI2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Lzcnt.IsSupported); Assert.False(Lzcnt.IsSupported, "LZCNT should be disabled when DisableHWIntrinsic is set.");
Assert.False(AdvSimd.IsSupported); Assert.False(AdvSimd.IsSupported, "Arm64 AdvSimd should be disabled when DisableHWIntrinsic is set.");
Assert.False(System.Runtime.Intrinsics.Arm.Aes.IsSupported); Assert.False(System.Runtime.Intrinsics.Arm.Aes.IsSupported, "Arm64 AES should be disabled when DisableHWIntrinsic is set.");
Assert.False(Crc32.IsSupported); Assert.False(Crc32.IsSupported, "Arm64 CRC32 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Dp.IsSupported); Assert.False(Dp.IsSupported, "Arm64 DotProd (DP) should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sha1.IsSupported); Assert.False(Sha1.IsSupported, "Arm64 SHA1 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sha256.IsSupported); Assert.False(Sha256.IsSupported, "Arm64 SHA256 should be disabled when DisableHWIntrinsic is set.");
break;
case HwIntrinsics.DisableSSE:
Assert.False(Sse.IsSupported);
break;
case HwIntrinsics.DisableSSE2:
Assert.False(Sse2.IsSupported);
break; break;
case HwIntrinsics.DisableAES: case HwIntrinsics.DisableAES:
Assert.False(Aes.IsSupported); Assert.False(Aes.IsSupported, "AES (x86) should be disabled when DisableAES is set.");
break; #if NET10_0_OR_GREATER
case HwIntrinsics.DisablePCLMULQDQ: Assert.False(Pclmulqdq.IsSupported, "PCLMULQDQ should be disabled when DisableAES is set (paired disable).");
Assert.False(Pclmulqdq.IsSupported); #endif
break;
case HwIntrinsics.DisableSSE3:
Assert.False(Sse3.IsSupported);
break;
case HwIntrinsics.DisableSSSE3:
Assert.False(Ssse3.IsSupported);
break;
case HwIntrinsics.DisableSSE41:
Assert.False(Sse41.IsSupported);
break; break;
case HwIntrinsics.DisableSSE42: case HwIntrinsics.DisableSSE42:
Assert.False(Sse42.IsSupported); #if NET10_0_OR_GREATER
break; Assert.False(Sse3.IsSupported, "Sse3 should be disabled.");
case HwIntrinsics.DisablePOPCNT: Assert.False(Ssse3.IsSupported, "Ssse3 should be disabled.");
Assert.False(Popcnt.IsSupported); Assert.False(Sse41.IsSupported, "Sse41 should be disabled.");
Assert.False(Popcnt.IsSupported, "Popcnt should be disabled.");
#else
Assert.False(Sse42.IsSupported, "Sse42 should be disabled when DisableSSE42 is set.");
#endif
break; break;
case HwIntrinsics.DisableAVX: case HwIntrinsics.DisableAVX:
Assert.False(Avx.IsSupported); Assert.False(Avx.IsSupported, "AVX should be disabled when DisableAVX is set.");
break;
case HwIntrinsics.DisableFMA:
Assert.False(Fma.IsSupported);
break; break;
case HwIntrinsics.DisableAVX2: case HwIntrinsics.DisableAVX2:
Assert.False(Avx2.IsSupported); Assert.False(Avx2.IsSupported, "AVX2 should be disabled when DisableAVX2 is set.");
break; #if NET10_0_OR_GREATER
case HwIntrinsics.DisableBMI1: Assert.False(Fma.IsSupported, "FMA should be disabled when DisableAVX2 is set (paired disable).");
Assert.False(Bmi1.IsSupported); Assert.False(Bmi1.IsSupported, "BMI1 should be disabled when DisableAVX2 is set (paired disable).");
break; Assert.False(Bmi2.IsSupported, "BMI2 should be disabled when DisableAVX2 is set (paired disable).");
case HwIntrinsics.DisableBMI2: Assert.False(Lzcnt.IsSupported, "LZCNT should be disabled when DisableAVX2 is set (paired disable).");
Assert.False(Bmi2.IsSupported); #endif
break;
case HwIntrinsics.DisableLZCNT:
Assert.False(Lzcnt.IsSupported);
break;
case HwIntrinsics.DisableArm64AdvSimd:
Assert.False(AdvSimd.IsSupported);
break; break;
case HwIntrinsics.DisableArm64Aes: case HwIntrinsics.DisableArm64Aes:
Assert.False(System.Runtime.Intrinsics.Arm.Aes.IsSupported); Assert.False(System.Runtime.Intrinsics.Arm.Aes.IsSupported, "Arm64 AES should be disabled when DisableArm64Aes is set.");
break; break;
case HwIntrinsics.DisableArm64Crc32: case HwIntrinsics.DisableArm64Crc32:
Assert.False(Crc32.IsSupported); Assert.False(Crc32.IsSupported, "Arm64 CRC32 should be disabled when DisableArm64Crc32 is set.");
break; break;
case HwIntrinsics.DisableArm64Dp: case HwIntrinsics.DisableArm64Dp:
Assert.False(Dp.IsSupported); Assert.False(Dp.IsSupported, "Arm64 DotProd (DP) should be disabled when DisableArm64Dp is set.");
break; break;
case HwIntrinsics.DisableArm64Sha1: case HwIntrinsics.DisableArm64Sha1:
Assert.False(Sha1.IsSupported); Assert.False(Sha1.IsSupported, "Arm64 SHA1 should be disabled when DisableArm64Sha1 is set.");
break; break;
case HwIntrinsics.DisableArm64Sha256: case HwIntrinsics.DisableArm64Sha256:
Assert.False(Sha256.IsSupported); Assert.False(Sha256.IsSupported, "Arm64 SHA256 should be disabled when DisableArm64Sha256 is set.");
break; break;
} }
} }
@ -189,12 +169,12 @@ public class FeatureTestRunnerTests
{ {
Assert.NotNull(serializable); Assert.NotNull(serializable);
Assert.NotNull(FeatureTestRunner.DeserializeForXunit<FakeSerializable>(serializable)); Assert.NotNull(FeatureTestRunner.DeserializeForXunit<FakeSerializable>(serializable));
Assert.False(Sse.IsSupported); Assert.False(Sse42.IsSupported, "SSE42 should be disabled when DisableSSE42 is set (sanity check using serializable param overload).");
} }
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
AssertHwIntrinsicsFeatureDisabled, AssertHwIntrinsicsFeatureDisabled,
HwIntrinsics.DisableSSE, HwIntrinsics.DisableSSE42,
new FakeSerializable()); new FakeSerializable());
} }
@ -209,90 +189,69 @@ public class FeatureTestRunnerTests
switch (Enum.Parse<HwIntrinsics>(intrinsic)) switch (Enum.Parse<HwIntrinsics>(intrinsic))
{ {
case HwIntrinsics.DisableHWIntrinsic: case HwIntrinsics.DisableHWIntrinsic:
Assert.False(Sse.IsSupported); Assert.False(Sse.IsSupported, "SSE should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse2.IsSupported); Assert.False(Sse2.IsSupported, "SSE2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Aes.IsSupported); Assert.False(Aes.IsSupported, "AES (x86) should be disabled when DisableHWIntrinsic is set.");
Assert.False(Pclmulqdq.IsSupported); Assert.False(Pclmulqdq.IsSupported, "PCLMULQDQ should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse3.IsSupported); Assert.False(Sse3.IsSupported, "SSE3 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Ssse3.IsSupported); Assert.False(Ssse3.IsSupported, "SSSE3 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse41.IsSupported); Assert.False(Sse41.IsSupported, "SSE4.1 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse42.IsSupported); Assert.False(Sse42.IsSupported, "SSE4.2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Popcnt.IsSupported); Assert.False(Popcnt.IsSupported, "POPCNT should be disabled when DisableHWIntrinsic is set.");
Assert.False(Avx.IsSupported); Assert.False(Avx.IsSupported, "AVX should be disabled when DisableHWIntrinsic is set.");
Assert.False(Fma.IsSupported); Assert.False(Fma.IsSupported, "FMA should be disabled when DisableHWIntrinsic is set.");
Assert.False(Avx2.IsSupported); Assert.False(Avx2.IsSupported, "AVX2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Bmi1.IsSupported); Assert.False(Bmi1.IsSupported, "BMI1 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Bmi2.IsSupported); Assert.False(Bmi2.IsSupported, "BMI2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Lzcnt.IsSupported); Assert.False(Lzcnt.IsSupported, "LZCNT should be disabled when DisableHWIntrinsic is set.");
Assert.False(AdvSimd.IsSupported); Assert.False(AdvSimd.IsSupported, "Arm64 AdvSimd should be disabled when DisableHWIntrinsic is set.");
Assert.False(System.Runtime.Intrinsics.Arm.Aes.IsSupported); Assert.False(System.Runtime.Intrinsics.Arm.Aes.IsSupported, "Arm64 AES should be disabled when DisableHWIntrinsic is set.");
Assert.False(Crc32.IsSupported); Assert.False(Crc32.IsSupported, "Arm64 CRC32 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Dp.IsSupported); Assert.False(Dp.IsSupported, "Arm64 DotProd (DP) should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sha1.IsSupported); Assert.False(Sha1.IsSupported, "Arm64 SHA1 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sha256.IsSupported); Assert.False(Sha256.IsSupported, "Arm64 SHA256 should be disabled when DisableHWIntrinsic is set.");
break;
case HwIntrinsics.DisableSSE:
Assert.False(Sse.IsSupported);
break;
case HwIntrinsics.DisableSSE2:
Assert.False(Sse2.IsSupported);
break; break;
case HwIntrinsics.DisableAES: case HwIntrinsics.DisableAES:
Assert.False(Aes.IsSupported); Assert.False(Aes.IsSupported, "AES (x86) should be disabled when DisableAES is set.");
break; #if NET10_0_OR_GREATER
case HwIntrinsics.DisablePCLMULQDQ: Assert.False(Pclmulqdq.IsSupported, "PCLMULQDQ should be disabled when DisableAES is set (paired disable).");
Assert.False(Pclmulqdq.IsSupported); #endif
break;
case HwIntrinsics.DisableSSE3:
Assert.False(Sse3.IsSupported);
break;
case HwIntrinsics.DisableSSSE3:
Assert.False(Ssse3.IsSupported);
break;
case HwIntrinsics.DisableSSE41:
Assert.False(Sse41.IsSupported);
break; break;
case HwIntrinsics.DisableSSE42: case HwIntrinsics.DisableSSE42:
Assert.False(Sse42.IsSupported); #if NET10_0_OR_GREATER
break; Assert.False(Sse3.IsSupported, "Sse3 should be disabled.");
case HwIntrinsics.DisablePOPCNT: Assert.False(Ssse3.IsSupported, "Ssse3 should be disabled.");
Assert.False(Popcnt.IsSupported); Assert.False(Sse41.IsSupported, "Sse41 should be disabled.");
Assert.False(Popcnt.IsSupported, "Popcnt should be disabled.");
#endif
Assert.False(Sse42.IsSupported, "Sse42 should be disabled when DisableSSE42 is set.");
break; break;
case HwIntrinsics.DisableAVX: case HwIntrinsics.DisableAVX:
Assert.False(Avx.IsSupported); Assert.False(Avx.IsSupported, "AVX should be disabled when DisableAVX is set.");
break;
case HwIntrinsics.DisableFMA:
Assert.False(Fma.IsSupported);
break; break;
case HwIntrinsics.DisableAVX2: case HwIntrinsics.DisableAVX2:
Assert.False(Avx2.IsSupported); Assert.False(Avx2.IsSupported, "AVX2 should be disabled when DisableAVX2 is set.");
break; #if NET10_0_OR_GREATER
case HwIntrinsics.DisableBMI1: Assert.False(Fma.IsSupported, "FMA should be disabled when DisableAVX2 is set (paired disable).");
Assert.False(Bmi1.IsSupported); Assert.False(Bmi1.IsSupported, "BMI1 should be disabled when DisableAVX2 is set (paired disable).");
break; Assert.False(Bmi2.IsSupported, "BMI2 should be disabled when DisableAVX2 is set (paired disable).");
case HwIntrinsics.DisableBMI2: Assert.False(Lzcnt.IsSupported, "LZCNT should be disabled when DisableAVX2 is set (paired disable).");
Assert.False(Bmi2.IsSupported); #endif
break;
case HwIntrinsics.DisableLZCNT:
Assert.False(Lzcnt.IsSupported);
break;
case HwIntrinsics.DisableArm64AdvSimd:
Assert.False(AdvSimd.IsSupported);
break; break;
case HwIntrinsics.DisableArm64Aes: case HwIntrinsics.DisableArm64Aes:
Assert.False(System.Runtime.Intrinsics.Arm.Aes.IsSupported); Assert.False(System.Runtime.Intrinsics.Arm.Aes.IsSupported, "Arm64 AES should be disabled when DisableArm64Aes is set.");
break; break;
case HwIntrinsics.DisableArm64Crc32: case HwIntrinsics.DisableArm64Crc32:
Assert.False(Crc32.IsSupported); Assert.False(Crc32.IsSupported, "Arm64 CRC32 should be disabled when DisableArm64Crc32 is set.");
break; break;
case HwIntrinsics.DisableArm64Dp: case HwIntrinsics.DisableArm64Dp:
Assert.False(Dp.IsSupported); Assert.False(Dp.IsSupported, "Arm64 DotProd (DP) should be disabled when DisableArm64Dp is set.");
break; break;
case HwIntrinsics.DisableArm64Sha1: case HwIntrinsics.DisableArm64Sha1:
Assert.False(Sha1.IsSupported); Assert.False(Sha1.IsSupported, "Arm64 SHA1 should be disabled when DisableArm64Sha1 is set.");
break; break;
case HwIntrinsics.DisableArm64Sha256: case HwIntrinsics.DisableArm64Sha256:
Assert.False(Sha256.IsSupported); Assert.False(Sha256.IsSupported, "Arm64 SHA256 should be disabled when DisableArm64Sha256 is set.");
break; break;
} }
} }

3
tests/Images/External/ReferenceOutput/AffineTransformTests/Issue3000_Rgba32_issue_3000_p-3-3.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d75205909d532dc98da52389c804ff99cb3b796b5657afb521659fe221c2b8f0
size 122

3
tests/Images/External/ReferenceOutput/AffineTransformTests/Issue3000_Rgba32_issue_3000_p-4-4.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a75beaec77378de4abb09317afa56b8e99ecba0d1c8571cad31aa790afb1a687
size 123

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_FromSourceRectangle1_Rgba32_TestPattern96x48.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bbe1ffaf7b801fd92724438cc810fd0c5506e0a907b970c4f0bf5bec3627ca2a oid sha256:60b050406fda4ff347660e71cb28a9dfceb4b39532f62ee96cb61d2671d3cf00
size 551 size 340

3
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_FromSourceRectangle1_Rgba32_TestPattern96x48__original.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:74bf3b8655c7548f28c25b1e467992f691dc429f4b06e85cfd04a3b541825811
size 478

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_FromSourceRectangle2_Rgba32_TestPattern96x48.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b45933471a1af1b6d4112240e1bc6b6187065a872043ddbf917200ce9e8cc84b oid sha256:fbfb3143d96070c58c949e8d1e8d9ddbcf1e7863514489ea2defc65863c84e73
size 371 size 276

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(0)_S(1,2)_T(0,0).png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b66a5f9d8a7f3f2a78b868bec6c7d1deea927b82d81aa6d1677e0461a3920dc9 oid sha256:120b661bef4adac64d362d8c987b3427cd8140ccac7404d09a16765ba1199434
size 3800 size 5191

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(0)_S(2,1)_T(0,0).png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d5fdc46ee866e088e0ec3221145a3d2d954a0bcb6d25cbb4d538978272f34949 oid sha256:0d668ebe5f8857fd21d7eb9ae86860751a6f3061f6c9f76705ff49216dc07870
size 4871 size 6215

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(-20,-10).png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5ae57ca0658b1ffa7aca9031f4ec065ab5a9813fb8a9c5acd221526df6a4f729 oid sha256:2fb676b3af585e7cbe2efdb893157d5f4e152cf810d0693cafb81596e941e121
size 9747 size 9697

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(0,0).png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0fced9def2b41cbbf215a49ea6ef6baf4c3c041fd180671eb209db5c6e7177e5 oid sha256:afe7ddbff155b918a4eff91af31e01100355c146cb9c8a12ab2496da8b22821d
size 10470 size 10446

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(20,10).png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1e4cc16c2f1b439f8780dead04db01fed95f8e20b68270ae8e7a988af999e3db oid sha256:ad76301984e5b54eae374adfe130693667053fbed181847b4c68688fb74c9818
size 10561 size 10518

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.1,1.3)_T(30,-20).png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:06e3966550f1c3ae72796e5522f7829cf1f86daca469c479acf49e6fae72e3d0 oid sha256:fbd57af1fa982f9090f57d820a9b927f894914e5f54774e9cd6fdcfe14e5f761
size 13227 size 13139

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.5,1.5)_T(0,0).png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8ce5fefe04cc2a036fddcfcf038901a7a09b4ea5d0621a1e0d3abc8430953ae3 oid sha256:c4bbc28c203550baf885cefba95c48a3f91dfb5242c09acbf3a8509b7258048e
size 20778 size 20768

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScale_ManuallyCentered_Rgba32_TestPattern96x96_R(50)_S(0.8).png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b653c0fe761d351cb15b09f35da578a954d103dea7507e2c1d7c4ebf3bdac49a oid sha256:566e85b1a527f48c953bcc7bc6c58ebd1fe0b14972c38edd596b025e0dd48624
size 10943 size 10940

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3a17bb1653cc6d6ecc292ce0670c651bfea032f61c6a0e84636205bde53a86f8 oid sha256:aa5b0d5de93f26c0a7a03b57a00d4a49cda62f4a4b98b6d374261467c03a8357
size 13536 size 13500

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b8970378312c0d479d618e4d5b8da54175c127db517fbe54f9057188d02cc735 oid sha256:62267d8d56af3e1452f0e25144f2cfe352b88def98af28e819a3a6982040a4ca
size 4165 size 4102

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3a17bb1653cc6d6ecc292ce0670c651bfea032f61c6a0e84636205bde53a86f8 oid sha256:878d5c53b84af4d133825206a327fd4cd02a43831ecabf5c61c5d89181c5a107
size 13536 size 13499

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9bbf7ef00f98b410f309b3bf70ce87d3c6455666a26e89cd004744145a10408a oid sha256:f0aa3c19852632e603ec425aeecc5243d4c6c24a1ac6e3906d29913bf7ead2df
size 12559 size 12535

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7f9ab86abad276d58bb029bd8e2c2aaffac5618322788cb3619577c7643e10d2 oid sha256:ec648c2e8006d478ace4a78d2434a4ef7f10d4a3502468cd8b9e2b1f154620b6
size 14223 size 14278

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:05c4dc9af1fef422fd5ada2fa1459c26253e0fb5e5a13226fa2e7445ece32272 oid sha256:6cb06152d5a0765ad86e8005d6ddac469914ccced89d5ee37d77e7d030b97c9e
size 17927 size 17281

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:82b47e1cad2eea417b99a2e4b68a5ba1a6cd6703f360e8402f3dca8b92373ecc oid sha256:38ea8596a682be0075bb31ed176b1fe04b518eb887235d551a574e338d45880b
size 18945 size 18869

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b15ce5a201ee6b946de485a58d3d8e779b6841457e096b2bd7a92968a122f9af oid sha256:965f42f021c63a0f2ccc691723c4ad7f92119294aec407c7ffd46a6238c8f268
size 20844 size 20792

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a1622a48b3f4790d66b229ed29acd18504cedf68d0a548832665c28d47ea663b oid sha256:86f1b9e8f1e38070ce862d87c927313904ceaa9e6080f5acead90e82d164738c
size 13857 size 13879

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:74df7b82e2148cfc8dae7e05c96009c0d70c09bf39cdc5ef9d727063d2a8cb3f oid sha256:270f9c2bf5d15fcb21796b3b9eb393e0cc00d9c337036637295ad1efb56781b1
size 4154 size 4114

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:cc740ccd76910e384ad84a780591652ac7ee0ea30abf7fd7f5b146f8ff380f07 oid sha256:d413162a83c223124a2f29f8154e4bdc08d94bd3e15569ec6cffaa13bdda72c8
size 13991 size 13953

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ccdc54e814604d4d339f6083091abf852aae65052ceb731af998208faddb5b0b oid sha256:941ea7b4d1f2c187f58920546e2f19fc275505929439cc389edcc59e652e8787
size 13744 size 13777

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:cd24e0a52c7743ab7d3ed255e3757c2d5495b3f56198556a157df589b1fb67ca oid sha256:cc5e6a607ef2343cb74c5227dbc7861840db956951f1fc4703fe53dbccda0974
size 14889 size 14808

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:878f1aab39b0b2405498c24146b8f81248b37b974e5ea7882e96174a034b645f oid sha256:2ac06a9ba2b2c8bef7e0117ac52fbb790101c0f89313dc49feb1f5a1d929ab02
size 12374 size 12381

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:dcc2bf4f7e0ab3d56ee71ac1e1855dababeb2e4ec167fd5dc264efdc9e727328 oid sha256:ad0f483fa7fda620860858c4f330ba914480fba15d70b408fb1aa3fed52dbfc1
size 17027 size 16839

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6c733878f4c0cc6075a01fbe7cb471f8b3e91c2c5eaf89309ea3c073d9cc4921 oid sha256:ba501a7fc32a68f8989965aa6457b3860ec42947e2bcd4526c7570ff743f38fc
size 854 size 841

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:af872886136893938aee82b1ac73e7a1820666a9a5f4bbf34159c09b3283169a oid sha256:8265c5b2e8edd5eaf0aeeccf86cac486e7beec581e696d3b4f4cfee8f4be9b2b
size 5520 size 5554

4
tests/Images/External/ReferenceOutput/Drawing/DrawImageTests/DrawTransformed.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0ba180567e820b145a13c9b26db9c777e95126adfe8e8cacec0ffe1060dcfe8d oid sha256:7f8a4db4facce1d68b363a3b59ea40c9da9fa3c989c736d97a703c84d8230660
size 184124 size 184595

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=-50, Y=-50 ]-PointF [ X=200, Y=-50 ]-PointF [ X=200, Y=200 ]-PointF [ X=-50, Y=200 ].png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:abce6af307a81a8ebac8e502142b00b2615403b5570c8dbe7b6895cfdd1a6d60 oid sha256:ac986987f25d25ab964a5bef710fe81166cb643d85511906218b4f0e72e9e840
size 66879 size 30532

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=0, Y=0 ]-PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ].png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d4cda265a50aa26711efafdbcd947c9a01eff872611df5298920583f9a3d4224 oid sha256:e0ada2a4d32a3a757b803dbf08148f113f5d358b31af79a77e97c660ce96c302
size 26458 size 1608

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ]-PointF [ X=0, Y=0 ].png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:278a488a858b8eda141493fe00c617eb1f664196853da8341d7e5b7f231ddce4 oid sha256:ffc30373989ec6857797b460931f011b30baaec633b095b6fc3d8fd5d43c77ec
size 24645 size 2467

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=25, Y=50 ]-PointF [ X=210, Y=25 ]-PointF [ X=140, Y=210 ]-PointF [ X=15, Y=125 ].png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e03e79e6fab3a9e43041e54640a04c7cc3677709e7d879f9f410cf8afc7547a7 oid sha256:9828ef0faf1a6709673cfe39028ed4202920d346bcc172bda6683bb3d1d0a7a3
size 42691 size 36577

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:543dbf5376386bf518830850645d69934e2ca17ab208ce3fd5274a6a172f5206 oid sha256:6bff913e6e67129325203fae91278ca17407b10d99c4e4f571e6cfe3b5b7f93c
size 10951 size 10889

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0d0cf291ebf5d8cebab1cd76e2830e5e2d2e0d9a050f7187da72680ead39110c oid sha256:54b761b76d03216e7aa6238eee92755c03f7b016bffd1400be66ecf136b29c26
size 2757 size 2747

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:543dbf5376386bf518830850645d69934e2ca17ab208ce3fd5274a6a172f5206 oid sha256:16da371a29269dade522b3d602beee8f769723c5712a348d960805b75619376d
size 10951 size 10889

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

Loading…
Cancel
Save