Browse Source

Merge branch 'main' into sn/fix_rider-errors

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

6
Directory.Build.props

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

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

@ -71,6 +71,11 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
/// </summary>
private bool hasAdobeMarker;
/// <summary>
/// Whether the image has a SOS marker.
/// </summary>
private bool hasSOSMarker;
/// <summary>
/// Contains information about the JFIF marker.
/// </summary>
@ -197,6 +202,12 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
{
using SpectralConverter<TPixel> spectralConverter = new(this.configuration, this.resizeMode == JpegDecoderResizeMode.ScaleOnly ? null : this.Options.TargetSize);
this.ParseStream(stream, spectralConverter, cancellationToken);
if (!this.hasSOSMarker)
{
JpegThrowHelper.ThrowInvalidImageContentException("Missing SOS marker.");
}
this.InitExifProfile();
this.InitIccProfile();
this.InitIptcProfile();
@ -215,6 +226,12 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.ParseStream(stream, spectralConverter: null, cancellationToken);
if (!this.hasSOSMarker)
{
JpegThrowHelper.ThrowInvalidImageContentException("Missing SOS marker.");
}
this.InitExifProfile();
this.InitIccProfile();
this.InitIptcProfile();
@ -403,6 +420,8 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
break;
case JpegConstants.Markers.SOS:
this.hasSOSMarker = true;
if (!metadataOnly)
{
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.RepeatCount = features.AnimationLoopCount;
Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore
this.webpMetadata.BackgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore
? Color.Transparent
: features.AnimationBackgroundColor!.Value;
this.webpMetadata.BackgroundColor = backgroundColor;
bool ignoreMetadata = this.skipMetadata;
SegmentIntegrityHandling segmentIntegrityHandling = this.segmentIntegrityHandling;
Span<byte> buffer = stackalloc byte[4];
uint frameCount = 0;
int remainingBytes = (int)completeDataSize;
@ -135,9 +135,16 @@ internal class WebpAnimationDecoder : IDisposable
remainingBytes -= (int)dataSize;
break;
case WebpChunkType.Iccp:
case WebpChunkType.Xmp:
case WebpChunkType.Exif:
WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, this.metadata, this.skipMetadata, this.segmentIntegrityHandling, buffer);
WebpChunkParsingUtils.ParseOptionalChunks(
stream,
chunkType,
this.metadata,
ignoreMetadata,
segmentIntegrityHandling,
buffer);
break;
default:
@ -187,9 +194,12 @@ internal class WebpAnimationDecoder : IDisposable
this.webpMetadata.BackgroundColor = backgroundColor;
TPixel backgroundPixel = backgroundColor.ToPixel<TPixel>();
bool ignoreMetadata = this.skipMetadata;
SegmentIntegrityHandling segmentIntegrityHandling = this.segmentIntegrityHandling;
Span<byte> buffer = stackalloc byte[4];
uint frameCount = 0;
int remainingBytes = (int)completeDataSize;
while (remainingBytes > 0)
{
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer);
@ -209,9 +219,10 @@ internal class WebpAnimationDecoder : IDisposable
remainingBytes -= (int)dataSize;
break;
case WebpChunkType.Iccp:
case WebpChunkType.Xmp:
case WebpChunkType.Exif:
WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, this.skipMetadata, this.segmentIntegrityHandling, buffer);
WebpChunkParsingUtils.ParseOptionalChunks(stream, chunkType, image!.Metadata, ignoreMetadata, segmentIntegrityHandling, buffer);
break;
default:

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

@ -9,6 +9,7 @@ using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp;
@ -258,6 +259,9 @@ internal static class WebpChunkParsingUtils
/// <param name="stream">The stream to read from.</param>
/// <param name="buffer">The buffer to store the read data into.</param>
/// <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)
{
if (stream.Read(buffer, 0, 3) == 3)
@ -272,8 +276,11 @@ internal static class WebpChunkParsingUtils
/// <summary>
/// Writes a unsigned 24 bit integer.
/// </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>
/// <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)
{
if (data >= 1 << 24)
@ -296,18 +303,24 @@ internal static class WebpChunkParsingUtils
/// </summary>
/// <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="required">If true, the chunk size is required to be read, otherwise it can be skipped.</param>
/// <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)
{
uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
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>
@ -320,14 +333,13 @@ internal static class WebpChunkParsingUtils
/// </exception>
public static WebpChunkType ReadChunkType(BufferedReadStream stream, Span<byte> buffer)
{
DebugGuard.IsTrue(buffer.Length == 4, "buffer has wrong length");
if (stream.Read(buffer) == 4)
{
WebpChunkType chunkType = (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer);
return chunkType;
return (WebpChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer);
}
// 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.");
}
@ -336,82 +348,182 @@ internal static class WebpChunkParsingUtils
/// 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.
/// </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(
BufferedReadStream stream,
WebpChunkType chunkType,
ImageMetadata metadata,
bool ignoreMetaData,
bool ignoreMetadata,
SegmentIntegrityHandling segmentIntegrityHandling,
Span<byte> buffer)
{
long streamLength = stream.Length;
while (stream.Position < streamLength)
{
uint chunkLength = ReadChunkSize(stream, buffer);
if (ignoreMetaData)
{
stream.Skip((int)chunkLength);
}
int bytesRead;
switch (chunkType)
{
case WebpChunkType.Exif:
byte[] exifData = new byte[chunkLength];
bytesRead = stream.Read(exifData, 0, (int)chunkLength);
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.Iccp:
ReadIccProfile(stream, metadata, ignoreMetadata, segmentIntegrityHandling, buffer);
break;
case WebpChunkType.Exif:
ReadExifProfile(stream, metadata, ignoreMetadata, segmentIntegrityHandling, buffer);
break;
case WebpChunkType.Xmp:
byte[] xmpData = new byte[chunkLength];
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);
ReadXmpProfile(stream, metadata, ignoreMetadata, segmentIntegrityHandling, buffer);
break;
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);
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)
{
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.Binary;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
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;
namespace SixLabors.ImageSharp.Formats.Webp;
@ -248,7 +244,8 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
else
{
// 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);
}
}
@ -279,18 +276,20 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
bool ignoreAlpha,
Span<byte> buffer)
{
bool ignoreMetadata = this.skipMetadata;
SegmentIntegrityHandling integrityHandling = this.segmentIntegrityHandling;
switch (chunkType)
{
case WebpChunkType.Iccp:
this.ReadIccProfile(stream, metadata, buffer);
WebpChunkParsingUtils.ReadIccProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer);
break;
case WebpChunkType.Exif:
this.ReadExifProfile(stream, metadata, buffer);
WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer);
break;
case WebpChunkType.Xmp:
this.ReadXmpProfile(stream, metadata, buffer);
WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer);
break;
case WebpChunkType.AnimationParameter:
@ -319,7 +318,10 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
/// <param name="buffer">Temporary buffer.</param>
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;
}
@ -328,139 +330,24 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
while (stream.Position < streamLength)
{
// Read chunk header.
WebpChunkType chunkType = ReadChunkType(stream, buffer);
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer);
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)
{
this.ReadXmpProfile(stream, metadata, buffer);
WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata, integrityHandling, buffer);
}
else
{
// Skip duplicate XMP or EXIF chunk.
uint chunkLength = ReadChunkSize(stream, buffer);
uint chunkLength = WebpChunkParsingUtils.ReadChunkSize(stream, buffer, false);
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>
/// Reads the animation parameters chunk from the stream.
/// </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/>
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>
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)
{
@ -66,6 +71,13 @@ public abstract partial class Image
/// <exception cref="UnknownImageFormatException">The encoded image format is unknown.</exception>
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)
{
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>
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)
{
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)
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)
{
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));
if (data.IsEmpty)
{
throw new ArgumentException("Pixel data cannot be empty.", nameof(data));
}
int count = width * height;
Guard.MustBeGreaterThanOrEqualTo(data.Length, count, nameof(data));

2
src/ImageSharp/ImageSharp.csproj

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

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

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

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.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public bool IsEmpty => this.Equals(Empty);
public readonly bool IsEmpty => this.Equals(Empty);
/// <summary>
/// Creates a <see cref="PointF"/> with the coordinates of the specified <see cref="Point"/>.
@ -239,7 +239,7 @@ public struct Point : IEquatable<Point>
/// </summary>
/// <param name="x">The out value for X.</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;
y = this.Y;
@ -268,17 +268,17 @@ public struct Point : IEquatable<Point>
public void Offset(Point point) => this.Offset(point.X, point.Y);
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.X, this.Y);
public override readonly int GetHashCode() => HashCode.Combine(this.X, this.Y);
/// <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/>
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/>
[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));

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.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public bool IsEmpty => this.Equals(Empty);
public readonly bool IsEmpty => this.Equals(Empty);
/// <summary>
/// Creates a <see cref="Vector2"/> with the coordinates of the specified <see cref="PointF"/>.
@ -251,7 +251,7 @@ public struct PointF : IEquatable<PointF>
/// </summary>
/// <param name="x">The out value for X.</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;
y = this.Y;
@ -277,15 +277,15 @@ public struct PointF : IEquatable<PointF>
public void Offset(PointF point) => this.Offset(point.X, point.Y);
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.X, this.Y);
public override readonly int GetHashCode() => HashCode.Combine(this.X, this.Y);
/// <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/>
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/>
[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.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public bool IsEmpty => this.Equals(Empty);
public readonly bool IsEmpty => this.Equals(Empty);
/// <summary>
/// Creates a <see cref="Vector2"/> with the coordinates of the specified <see cref="PointF"/>.
@ -201,24 +201,24 @@ public struct SizeF : IEquatable<SizeF>
/// </summary>
/// <param name="width">The out value for the width.</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;
height = this.Height;
}
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.Width, this.Height);
public override readonly int GetHashCode() => HashCode.Combine(this.Width, this.Height);
/// <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/>
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/>
[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>
/// 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.
/// </summary>
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>
/// Prepends a rotation matrix using the given rotation angle in degrees
/// and the image center point as rotation center.
@ -52,7 +37,7 @@ public class AffineTransformBuilder
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependRotationRadians(float radians)
=> this.Prepend(
size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace));
size => TransformUtilities.CreateRotationTransformMatrixRadians(radians, size));
/// <summary>
/// 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>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendRotationRadians(float radians)
=> this.Append(size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace));
=> this.Append(size => TransformUtilities.CreateRotationTransformMatrixRadians(radians, size));
/// <summary>
/// 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>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
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>
/// 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>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
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>
/// Appends a skew matrix using the given angles in degrees at the given origin.
@ -344,15 +329,29 @@ public class AffineTransformBuilder
/// for linear transforms.
/// </exception>
/// <returns>The <see cref="Size"/>.</returns>
public Size GetTransformedSize(Rectangle sourceRectangle)
public SizeF GetTransformedSize(Rectangle 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)
{
if (TransformUtils.IsDegenerate(matrix))
if (TransformUtilities.IsDegenerate(matrix))
{
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;
/// <summary>
/// Defines extensions that allow the application of composable transform operations on an <see cref="Image"/>
/// using Mutate/Clone.
/// Defines extensions that allow the application of composable transform operations
/// on an <see cref="IImageProcessingContext"/> using Mutate/Clone.
/// </summary>
public static class TransformExtensions
{
@ -51,7 +51,7 @@ public static class TransformExtensions
IResampler sampler)
{
Matrix3x2 transform = builder.BuildMatrix(sourceRectangle);
Size targetDimensions = builder.GetTransformedSize(sourceRectangle);
Size targetDimensions = TransformUtilities.GetTransformedCanvasSize(transform, sourceRectangle.Size);
return source.Transform(sourceRectangle, transform, targetDimensions, sampler);
}
@ -113,7 +113,7 @@ public static class TransformExtensions
IResampler sampler)
{
Matrix4x4 transform = builder.BuildMatrix(sourceRectangle);
Size targetDimensions = builder.GetTransformedSize(sourceRectangle);
Size targetDimensions = TransformUtilities.GetTransformedCanvasSize(transform, sourceRectangle.Size);
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.MustBeValueType(sampler);
if (TransformUtils.IsDegenerate(matrix))
if (TransformUtilities.IsDegenerate(matrix))
{
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;
}
// 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);
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.MustBeValueType(sampler);
if (TransformUtils.IsDegenerate(matrix))
if (TransformUtilities.IsDegenerate(matrix))
{
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;
}
// 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);
if (sampler is NearestNeighborResampler)
@ -135,7 +137,7 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
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 py = (int)MathF.Round(point.Y);
@ -207,7 +209,7 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
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 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>
public RotateProcessor(float degrees, IResampler sampler, Size sourceSize)
: this(
TransformUtils.CreateRotationTransformMatrixDegrees(degrees, sourceSize, TransformSpace.Pixel),
TransformUtilities.CreateRotationTransformMatrixDegrees(degrees, sourceSize),
sampler,
sourceSize)
=> this.Degrees = degrees;
// Helper constructor
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>
public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize)
: this(
TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize, TransformSpace.Pixel),
TransformUtilities.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize),
sampler,
sourceSize)
{
@ -40,7 +40,7 @@ public sealed class SkewProcessor : AffineTransformProcessor
// Helper constructor:
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
// to update any metadata that represents pixel coordinates in the source image.
this.transformMatrix = new ProjectiveTransformBuilder()
.AppendMatrix(TransformUtils.GetSwizzlerMatrix(swizzler, sourceRectangle))
.AppendMatrix(TransformUtilities.GetSwizzlerMatrix(swizzler, 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>
/// Contains utility methods for working with transforms.
/// </summary>
internal static class TransformUtils
internal static class TransformUtilities
{
/// <summary>
/// Returns a value that indicates whether the specified matrix is degenerate
@ -80,79 +80,69 @@ internal static class TransformUtils
}
/// <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>
/// <param name="degrees">The amount of rotation, in degrees.</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>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size, TransformSpace transformSpace)
=> CreateRotationTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degrees), size, transformSpace);
public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size)
=> CreateRotationTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degrees), size);
/// <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>
/// <param name="radians">The amount of rotation, in radians.</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>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size, TransformSpace transformSpace)
=> CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotation(radians, PointF.Empty), size, transformSpace);
public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size)
=> CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotation(radians, PointF.Empty), size);
/// <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>
/// <param name="degreesX">The X angle, in degrees.</param>
/// <param name="degreesY">The Y angle, in degrees.</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>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size, TransformSpace transformSpace)
=> CreateSkewTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), size, transformSpace);
public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size)
=> CreateSkewTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), size);
/// <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>
/// <param name="radiansX">The X angle, in radians.</param>
/// <param name="radiansY">The Y angle, in radians.</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>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size, TransformSpace transformSpace)
=> CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty), size, transformSpace);
public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size)
=> CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty), size);
/// <summary>
/// Gets the centered transform matrix based upon the source rectangle.
/// </summary>
/// <param name="matrix">The transformation matrix.</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>
[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);
// 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);
// 1) Unbounded size.
SizeF ts = GetRawTransformedSize(matrix, size);
// The source size is provided using the coordinate space of the source image.
// however the transform should always be applied in the pixel space.
// To account for this we offset by the size - 1 to translate to the pixel space.
float offset = transformSpace == TransformSpace.Pixel ? 1F : 0F;
// 2) Invert the content transform for screen->world.
Matrix3x2.Invert(matrix, out Matrix3x2 inv);
Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-(transformSize.Width - offset), -(transformSize.Height - offset)) * .5F);
Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(size.Width - offset, size.Height - offset) * .5F);
// 3) Translate target (canvas) so its center is at the origin,
// 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.
Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered);
// 4) World->screen.
Matrix3x2.Invert(toTarget * inv * toSource, out Matrix3x2 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="bottomRight">The bottom-right 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>
/// <remarks>
/// This method is based on the algorithm described in the following article:
@ -298,8 +287,7 @@ internal static class TransformUtils
PointF topLeft,
PointF topRight,
PointF bottomRight,
PointF bottomLeft,
TransformSpace transformSpace)
PointF bottomLeft)
{
PointF p1 = new(rectangle.X, 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);
#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;
}
/// <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>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when calculating the size.</param>
/// <returns>The <see cref="Size"/>.</returns>
public static Size GetTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace)
=> GetTransformedSize(matrix, size, transformSpace, true);
/// <param name="size">The original source size.</param>
/// <returns>
/// 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>
/// <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>
/// 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>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> used when generating the matrix.</param>
/// <param name="size">The original source size.</param>
/// <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>
/// <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)]
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!");
@ -393,27 +429,9 @@ internal static class TransformUtils
return size;
}
// 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.
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))
if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size), matrix, out RectangleF bounds))
{
// Add the offset size back to translate the transformed bounds to the correct space.
return Size.Ceiling(ConstrainSize(bounds) + offsetSize);
return preserveCanvas ? GetPreserveCanvasSize(bounds) : bounds.Size;
}
return size;
@ -438,30 +456,31 @@ internal static class TransformUtils
swizzler.Transform(new Point(sourceRectangle.Left, sourceRectangle.Top)),
swizzler.Transform(new Point(sourceRectangle.Right, sourceRectangle.Top)),
swizzler.Transform(new Point(sourceRectangle.Right, sourceRectangle.Bottom)),
swizzler.Transform(new Point(sourceRectangle.Left, sourceRectangle.Bottom)),
TransformSpace.Pixel);
swizzler.Transform(new Point(sourceRectangle.Left, sourceRectangle.Bottom)));
/// <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 source size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when calculating the size.</param>
/// <param name="size">The original source size.</param>
/// <returns>The <see cref="Size"/>.</returns>
private static Size GetUnboundedTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace)
=> GetTransformedSize(matrix, size, transformSpace, false);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SizeF GetRawTransformedSize(Matrix3x2 matrix, Size size)
=> GetTransformedSize(matrix, size, false);
/// <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>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when calculating the size.</param>
/// <param name="constrain">Whether to constrain the size to ensure that the dimensions are positive.</param>
/// <returns>
/// The <see cref="Size"/>.
/// </returns>
private static Size GetTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace, bool constrain)
/// <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)]
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!");
@ -470,21 +489,9 @@ internal static class TransformUtils
return size;
}
// Define an offset size to translate between coordinate space and pixel space.
// 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))
if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size), matrix, out RectangleF bounds))
{
// Add the offset size back to translate the transformed bounds to the coordinate space.
return Size.Ceiling((constrain ? ConstrainSize(bounds) : bounds.Size) + offsetSize);
return preserveCanvas ? GetPreserveCanvasSize(bounds) : bounds.Size;
}
return size;
@ -499,7 +506,8 @@ internal static class TransformUtils
/// <returns>
/// <see langword="true"/> if the transformation was successful; otherwise, <see langword="false"/>.
/// </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))
{
@ -526,7 +534,7 @@ internal static class TransformUtils
/// <see langword="true"/> if the transformation was successful; otherwise, <see langword="false"/>.
/// </returns>
[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))
{
@ -543,15 +551,61 @@ internal static class TransformUtils
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)]
private static Size ConstrainSize(Rectangle rectangle)
private static SizeF GetPreserveCanvasSize(RectangleF rectangle)
{
// We want to resize the canvas here taking into account any translations.
int height = rectangle.Top < 0 ? rectangle.Bottom : Math.Max(rectangle.Height, rectangle.Bottom);
int width = rectangle.Left < 0 ? rectangle.Right : Math.Max(rectangle.Width, rectangle.Right);
// If location in either direction is translated to a negative value equal to or exceeding the
// dimensions in either direction we need to reassign the dimension.
// Compute the required height.
// If the top is negative, expand upward by that amount (rectangle.Bottom already includes height).
// Otherwise, take the larger of the transformed height or the bottom offset.
float height = rectangle.Top < 0
? rectangle.Bottom
: 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)
{
height = rectangle.Height;
@ -562,63 +616,63 @@ internal static class TransformUtils
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)]
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 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 bottom = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y)));
// Clamp the values to the nearest whole pixel.
return Rectangle.FromLTRB(
(int)Math.Floor(left),
(int)Math.Floor(top),
(int)Math.Ceiling(right),
(int)Math.Ceiling(bottom));
return RectangleF.FromLTRB(left, top, right, 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])
if (Math.Abs(matrix.M14) > epsilon ||
Math.Abs(matrix.M24) > epsilon ||
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;
matrix.M31 += (-dx) + ((dx * matrix.M11) + (dy * matrix.M21));
matrix.M32 += (-dy) + ((dx * matrix.M12) + (dy * matrix.M22));
return matrix;
}
// Compute the determinant of the linear part
float determinant = (m11 * ((m22 * m33) - (m23 * m32))) -
(m12 * ((m21 * m33) - (m23 * m31))) +
(m13 * ((m21 * m32) - (m22 * m31)));
/// <summary>
/// Normalizes a projective 4×4 matrix so that it operates in pixel space.
/// Applies the row-vector conjugation <c>T(+0.5,+0.5,0) * M * T(-0.5,-0.5,0)</c>
/// 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)
if (Math.Abs(Math.Abs(determinant) - 1f) > epsilon)
// Fast path: affine (no perspective)
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)
return true;
Matrix4x4 tPos = Matrix4x4.Identity;
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.
/// </summary>
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>
/// Prepends a matrix that performs a tapering projective transform.
/// </summary>
@ -43,7 +28,7 @@ public class ProjectiveTransformBuilder
/// <param name="fraction">The amount to taper.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
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>
/// Appends a matrix that performs a tapering projective transform.
@ -53,7 +38,7 @@ public class ProjectiveTransformBuilder
/// <param name="fraction">The amount to taper.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
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>
/// 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>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
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>
/// 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>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
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>
/// 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>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
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>
/// 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>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
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>
/// 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>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
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>
/// 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>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft)
=> this.Prepend(size => TransformUtils.CreateQuadDistortionMatrix(
new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft, this.TransformSpace));
=> this.Prepend(size => TransformUtilities.CreateQuadDistortionMatrix(
new Rectangle(Point.Empty, size),
topLeft,
topRight,
bottomRight,
bottomLeft));
/// <summary>
/// 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>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft)
=> this.Append(size => TransformUtils.CreateQuadDistortionMatrix(
new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft, this.TransformSpace));
=> this.Append(size => TransformUtilities.CreateQuadDistortionMatrix(
new Rectangle(Point.Empty, size),
topLeft,
topRight,
bottomRight,
bottomLeft));
/// <summary>
/// Prepends a raw matrix.
@ -383,15 +377,29 @@ public class ProjectiveTransformBuilder
/// for linear transforms.
/// </exception>
/// <returns>The <see cref="Size"/>.</returns>
public Size GetTransformedSize(Rectangle sourceRectangle)
public SizeF GetTransformedSize(Rectangle 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)
{
if (TransformUtils.IsDegenerate(matrix))
if (TransformUtilities.IsDegenerate(matrix))
{
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
-->
<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="Moq" Version="4.20.72" />
<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,
as such, some of these rules cannot be implemented.
-->
<!--Mark members as static-->
<!--Validate platform compatibility-->
<!--Types that own disposable fields should be disposable-->
@ -39,7 +39,7 @@
<Choose>
<When Condition="$(SIXLABORS_TESTING_PREVIEW) == true">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
</PropertyGroup>
</When>
<Otherwise>

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

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

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

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

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

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

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

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

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

@ -152,7 +152,7 @@ public static class DCTTests
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
seed,
HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
}
[Theory]
@ -352,15 +352,14 @@ public static class DCTTests
Assert.Equal(expectedDest, actualDest, new ApproximateFloatComparer(1f));
}
// 4 paths:
// 1. AllowAll - call avx/fma implementation
// 2. DisableFMA - call avx without fma implementation
// 3. DisableAvx - call Vector4 implementation
// 4. DisableHWIntrinsic - call scalar fallback implementation
// 3 paths:
// 1. AllowAll - call avx implementation
// 2. DisableAvx - call Vector4 implementation
// 3. DisableHWIntrinsic - call scalar fallback implementation
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
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 HwIntrinsics IntrinsicsConfig = HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX2;
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 JpegColorConverterTests(ITestOutputHelper output)
@ -73,7 +69,7 @@ public class JpegColorConverterTests
{
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSE2 | HwIntrinsics.DisableHWIntrinsic);
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
static void RunTest(string arg)
{
@ -106,7 +102,7 @@ public class JpegColorConverterTests
{
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSE2 | HwIntrinsics.DisableHWIntrinsic);
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic);
static void RunTest(string arg)
{
@ -139,7 +135,7 @@ public class JpegColorConverterTests
{
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSE2 | HwIntrinsics.DisableHWIntrinsic);
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic);
static void RunTest(string arg)
{
@ -172,7 +168,7 @@ public class JpegColorConverterTests
{
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSE2 | HwIntrinsics.DisableHWIntrinsic);
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic);
static void RunTest(string arg)
{
@ -205,7 +201,7 @@ public class JpegColorConverterTests
{
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSE2 | HwIntrinsics.DisableHWIntrinsic);
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic);
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.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);
[Fact]
public void PaethFilter_WithoutSsse3_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPaethFilterTest, HwIntrinsics.DisableSSSE3);
public void PaethFilter_WithoutSsse3_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPaethFilterTest, HwIntrinsics.DisableHWIntrinsic);
[Fact]
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(
RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSSE3);
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic);
}
[Fact]

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

@ -618,7 +618,7 @@ public partial class PngEncoderTests
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
HwIntrinsics.DisableSSSE3,
HwIntrinsics.DisableHWIntrinsic,
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);
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]
@ -783,7 +785,7 @@ public class TiffDecoderTests : TiffDecoderBaseTester
// ImageMagick cannot decode this image.
image.DebugSave(provider);
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,
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);
[Fact]
public void CollectColorBlueTransforms_WithoutVector128_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCollectColorBlueTransformsTest, HwIntrinsics.DisableSSE41);
public void CollectColorBlueTransforms_WithoutVector128_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCollectColorBlueTransformsTest, HwIntrinsics.DisableHWIntrinsic);
[Fact]
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);
[Fact]
public void CollectColorRedTransforms_WithoutVector128_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCollectColorRedTransformsTest, HwIntrinsics.DisableSSE41);
public void CollectColorRedTransforms_WithoutVector128_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCollectColorRedTransformsTest, HwIntrinsics.DisableHWIntrinsic);
[Fact]
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);
[Fact]
public void Predictor11_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPredictor11Test, HwIntrinsics.DisableSSE2);
public void Predictor11_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPredictor11Test, HwIntrinsics.DisableHWIntrinsic);
[Fact]
public void Predictor12_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPredictor12Test, HwIntrinsics.AllowAll);
[Fact]
public void Predictor12_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPredictor12Test, HwIntrinsics.DisableSSE2);
public void Predictor12_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPredictor12Test, HwIntrinsics.DisableHWIntrinsic);
[Fact]
public void Predictor13_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPredictor13Test, HwIntrinsics.AllowAll);
[Fact]
public void Predictor13_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPredictor13Test, HwIntrinsics.DisableSSE2);
public void Predictor13_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPredictor13Test, HwIntrinsics.DisableHWIntrinsic);
[Fact]
public void SubtractGreen_Works() => RunSubtractGreenTest();
@ -331,7 +331,7 @@ public class LosslessUtilsTests
public void SubtractGreen_Scalar_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSubtractGreenTest, HwIntrinsics.DisableHWIntrinsic);
[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]
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);
[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]
public void TransformColor_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTransformColorTest, HwIntrinsics.AllowAll);
[Fact]
public void TransformColor_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTransformColorTest, HwIntrinsics.DisableSSE2);
public void TransformColor_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTransformColorTest, HwIntrinsics.DisableHWIntrinsic);
[Fact]
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);
[Fact]
public void TransformColorInverse_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTransformColorInverseTest, HwIntrinsics.DisableSSE2);
public void TransformColorInverse_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTransformColorInverseTest, HwIntrinsics.DisableHWIntrinsic);
[Fact]
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]
public void ColorSpaceTransform_WithPeakImage_WithoutSSE41_Works()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ColorSpaceTransform_WithPeakImage_ProducesExpectedData, HwIntrinsics.DisableSSE41);
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ColorSpaceTransform_WithPeakImage_ProducesExpectedData, HwIntrinsics.DisableHWIntrinsic);
[Fact]
public void ColorSpaceTransform_WithBikeImage_WithHardwareIntrinsics_Works()
@ -32,7 +32,7 @@ public class PredictorEncoderTests
[Fact]
public void ColorSpaceTransform_WithBikeImage_WithoutSSE41_Works()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ColorSpaceTransform_WithBikeImage_ProducesExpectedData, HwIntrinsics.DisableSSE41);
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ColorSpaceTransform_WithBikeImage_ProducesExpectedData, HwIntrinsics.DisableHWIntrinsic);
[Fact]
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);
[Fact]
public void QuantizeBlock_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunQuantizeBlockTest, HwIntrinsics.DisableSSE2);
public void QuantizeBlock_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunQuantizeBlockTest, HwIntrinsics.DisableHWIntrinsic);
[Fact]
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);
[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]
public void CheckNonOpaque_WithOpaquePixels_WithoutSse2_Works()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCheckNoneOpaqueWithOpaquePixelsTest, HwIntrinsics.DisableSSE2);
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCheckNoneOpaqueWithOpaquePixelsTest, HwIntrinsics.DisableHWIntrinsic);
[Fact]
public void CheckNonOpaque_WithOpaquePixels_WithoutAvx2_Works()
@ -35,7 +35,7 @@ public class WebpCommonUtilsTests
[Fact]
public void CheckNonOpaque_WithNoneOpaquePixels_WithoutSse2_Works()
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCheckNoneOpaqueWithNoneOpaquePixelsTest, HwIntrinsics.DisableSSE2);
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCheckNoneOpaqueWithNoneOpaquePixelsTest, HwIntrinsics.DisableHWIntrinsic);
[Fact]
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);
[Fact]
public void UpSampleYuvToRgb_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunUpSampleYuvToRgbTest, HwIntrinsics.DisableSSE2);
public void UpSampleYuvToRgb_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunUpSampleYuvToRgbTest, HwIntrinsics.DisableHWIntrinsic);
[Theory]
[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);
}
[Fact]
public void FromBytes_EmptySpan_Throws()
=> Assert.Throws<UnknownImageFormatException>(() => Image.DetectFormat([]));
[Fact]
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);
}
[Fact]
public void FromBytes_EmptySpan_Throws()
=> Assert.Throws<UnknownImageFormatException>(() => Image.Identify([]));
[Fact]
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);
}
[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);
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>
<When Condition="$(SIXLABORS_TESTING_PREVIEW) == true">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
</PropertyGroup>
</When>
<Otherwise>

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

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

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

@ -148,7 +148,7 @@ public class BokehBlurTest
[Theory]
[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)
{
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);
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]
@ -267,31 +283,41 @@ public class AffineTransformTests
image.CompareToReferenceOutput(ValidatorComparer, provider, testOutputDetails: radians);
}
[Fact]
public void TransformRotationDoesNotOffset()
[Theory]
[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 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.Mutate(c => c.Rotate(180));
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.Mutate(
c =>
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.Mutate(c => c.Transform(new AffineTransformBuilder().AppendRotationDegrees(180)));
img3.DebugSave(provider, "AffineRotate180Bicubic");
ImageComparer.Exact.VerifySimilarity(img, img2);
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));
// 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(
[94, 84],
[95, 85],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectLocation).Value);
// The subject area is also mirrored around the center.
// New X = imageWidth - originalX - width
// 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(
[44, 34, 50, 50],
[45, 35, 50, 50],
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);
Assert.Equal(
[2, 7, 11, 11],
[2, 7, 10, 10],
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));
// 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(
[94, 84],
[95, 85],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectLocation).Value);
// The subject area is also mirrored around the center.
// New X = imageWidth - originalX - width
// 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(
[44, 34, 50, 50],
[45, 35, 50, 50],
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(
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);
// 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 expected = Vector2.Transform(position, matrix);
@ -152,7 +152,7 @@ public abstract class TransformBuilderTestBase<TBuilder>
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 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
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 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 Issue2758 = "Jpg/issues/issue-2758.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
{

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

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

@ -47,7 +47,7 @@ public class FeatureTestRunnerTests
}
FeatureTestRunner.RunWithHwIntrinsicsFeature(
() => Assert.True(Vector.IsHardwareAccelerated),
() => Assert.True(Vector.IsHardwareAccelerated, "Vector hardware acceleration should be enabled when AllowAll is specified."),
HwIntrinsics.AllowAll);
}
@ -56,21 +56,21 @@ public class FeatureTestRunnerTests
{
static void AssertDisabled()
{
Assert.False(Sse.IsSupported);
Assert.False(Sse2.IsSupported);
Assert.False(Aes.IsSupported);
Assert.False(Pclmulqdq.IsSupported);
Assert.False(Sse3.IsSupported);
Assert.False(Ssse3.IsSupported);
Assert.False(Sse41.IsSupported);
Assert.False(Sse42.IsSupported);
Assert.False(Popcnt.IsSupported);
Assert.False(Avx.IsSupported);
Assert.False(Fma.IsSupported);
Assert.False(Avx2.IsSupported);
Assert.False(Bmi1.IsSupported);
Assert.False(Bmi2.IsSupported);
Assert.False(Lzcnt.IsSupported);
Assert.False(Sse.IsSupported, "SSE should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse2.IsSupported, "SSE2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Aes.IsSupported, "AES (x86) should be disabled when DisableHWIntrinsic is set.");
Assert.False(Pclmulqdq.IsSupported, "PCLMULQDQ should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse3.IsSupported, "SSE3 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Ssse3.IsSupported, "SSSE3 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse41.IsSupported, "SSE4.1 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse42.IsSupported, "SSE4.2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Popcnt.IsSupported, "POPCNT should be disabled when DisableHWIntrinsic is set.");
Assert.False(Avx.IsSupported, "AVX should be disabled when DisableHWIntrinsic is set.");
Assert.False(Fma.IsSupported, "FMA should be disabled when DisableHWIntrinsic is set.");
Assert.False(Avx2.IsSupported, "AVX2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Bmi1.IsSupported, "BMI1 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Bmi2.IsSupported, "BMI2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Lzcnt.IsSupported, "LZCNT should be disabled when DisableHWIntrinsic is set.");
}
FeatureTestRunner.RunWithHwIntrinsicsFeature(
@ -88,90 +88,70 @@ public class FeatureTestRunnerTests
switch (Enum.Parse<HwIntrinsics>(intrinsic))
{
case HwIntrinsics.DisableHWIntrinsic:
Assert.False(Sse.IsSupported);
Assert.False(Sse2.IsSupported);
Assert.False(Aes.IsSupported);
Assert.False(Pclmulqdq.IsSupported);
Assert.False(Sse3.IsSupported);
Assert.False(Ssse3.IsSupported);
Assert.False(Sse41.IsSupported);
Assert.False(Sse42.IsSupported);
Assert.False(Popcnt.IsSupported);
Assert.False(Avx.IsSupported);
Assert.False(Fma.IsSupported);
Assert.False(Avx2.IsSupported);
Assert.False(Bmi1.IsSupported);
Assert.False(Bmi2.IsSupported);
Assert.False(Lzcnt.IsSupported);
Assert.False(AdvSimd.IsSupported);
Assert.False(System.Runtime.Intrinsics.Arm.Aes.IsSupported);
Assert.False(Crc32.IsSupported);
Assert.False(Dp.IsSupported);
Assert.False(Sha1.IsSupported);
Assert.False(Sha256.IsSupported);
break;
case HwIntrinsics.DisableSSE:
Assert.False(Sse.IsSupported);
break;
case HwIntrinsics.DisableSSE2:
Assert.False(Sse2.IsSupported);
Assert.False(Sse.IsSupported, "SSE should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse2.IsSupported, "SSE2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Aes.IsSupported, "AES (x86) should be disabled when DisableHWIntrinsic is set.");
Assert.False(Pclmulqdq.IsSupported, "PCLMULQDQ should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse3.IsSupported, "SSE3 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Ssse3.IsSupported, "SSSE3 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse41.IsSupported, "SSE4.1 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse42.IsSupported, "SSE4.2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Popcnt.IsSupported, "POPCNT should be disabled when DisableHWIntrinsic is set.");
Assert.False(Avx.IsSupported, "AVX should be disabled when DisableHWIntrinsic is set.");
Assert.False(Fma.IsSupported, "FMA should be disabled when DisableHWIntrinsic is set.");
Assert.False(Avx2.IsSupported, "AVX2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Bmi1.IsSupported, "BMI1 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Bmi2.IsSupported, "BMI2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Lzcnt.IsSupported, "LZCNT should be disabled when DisableHWIntrinsic is set.");
Assert.False(AdvSimd.IsSupported, "Arm64 AdvSimd should be disabled when DisableHWIntrinsic is set.");
Assert.False(System.Runtime.Intrinsics.Arm.Aes.IsSupported, "Arm64 AES should be disabled when DisableHWIntrinsic is set.");
Assert.False(Crc32.IsSupported, "Arm64 CRC32 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Dp.IsSupported, "Arm64 DotProd (DP) should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sha1.IsSupported, "Arm64 SHA1 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sha256.IsSupported, "Arm64 SHA256 should be disabled when DisableHWIntrinsic is set.");
break;
case HwIntrinsics.DisableAES:
Assert.False(Aes.IsSupported);
break;
case HwIntrinsics.DisablePCLMULQDQ:
Assert.False(Pclmulqdq.IsSupported);
break;
case HwIntrinsics.DisableSSE3:
Assert.False(Sse3.IsSupported);
break;
case HwIntrinsics.DisableSSSE3:
Assert.False(Ssse3.IsSupported);
break;
case HwIntrinsics.DisableSSE41:
Assert.False(Sse41.IsSupported);
Assert.False(Aes.IsSupported, "AES (x86) should be disabled when DisableAES is set.");
#if NET10_0_OR_GREATER
Assert.False(Pclmulqdq.IsSupported, "PCLMULQDQ should be disabled when DisableAES is set (paired disable).");
#endif
break;
case HwIntrinsics.DisableSSE42:
Assert.False(Sse42.IsSupported);
break;
case HwIntrinsics.DisablePOPCNT:
Assert.False(Popcnt.IsSupported);
#if NET10_0_OR_GREATER
Assert.False(Sse3.IsSupported, "Sse3 should be disabled.");
Assert.False(Ssse3.IsSupported, "Ssse3 should be disabled.");
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;
case HwIntrinsics.DisableAVX:
Assert.False(Avx.IsSupported);
break;
case HwIntrinsics.DisableFMA:
Assert.False(Fma.IsSupported);
Assert.False(Avx.IsSupported, "AVX should be disabled when DisableAVX is set.");
break;
case HwIntrinsics.DisableAVX2:
Assert.False(Avx2.IsSupported);
break;
case HwIntrinsics.DisableBMI1:
Assert.False(Bmi1.IsSupported);
break;
case HwIntrinsics.DisableBMI2:
Assert.False(Bmi2.IsSupported);
break;
case HwIntrinsics.DisableLZCNT:
Assert.False(Lzcnt.IsSupported);
break;
case HwIntrinsics.DisableArm64AdvSimd:
Assert.False(AdvSimd.IsSupported);
Assert.False(Avx2.IsSupported, "AVX2 should be disabled when DisableAVX2 is set.");
#if NET10_0_OR_GREATER
Assert.False(Fma.IsSupported, "FMA should be disabled when DisableAVX2 is set (paired disable).");
Assert.False(Bmi1.IsSupported, "BMI1 should be disabled when DisableAVX2 is set (paired disable).");
Assert.False(Bmi2.IsSupported, "BMI2 should be disabled when DisableAVX2 is set (paired disable).");
Assert.False(Lzcnt.IsSupported, "LZCNT should be disabled when DisableAVX2 is set (paired disable).");
#endif
break;
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;
case HwIntrinsics.DisableArm64Crc32:
Assert.False(Crc32.IsSupported);
Assert.False(Crc32.IsSupported, "Arm64 CRC32 should be disabled when DisableArm64Crc32 is set.");
break;
case HwIntrinsics.DisableArm64Dp:
Assert.False(Dp.IsSupported);
Assert.False(Dp.IsSupported, "Arm64 DotProd (DP) should be disabled when DisableArm64Dp is set.");
break;
case HwIntrinsics.DisableArm64Sha1:
Assert.False(Sha1.IsSupported);
Assert.False(Sha1.IsSupported, "Arm64 SHA1 should be disabled when DisableArm64Sha1 is set.");
break;
case HwIntrinsics.DisableArm64Sha256:
Assert.False(Sha256.IsSupported);
Assert.False(Sha256.IsSupported, "Arm64 SHA256 should be disabled when DisableArm64Sha256 is set.");
break;
}
}
@ -189,12 +169,12 @@ public class FeatureTestRunnerTests
{
Assert.NotNull(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(
AssertHwIntrinsicsFeatureDisabled,
HwIntrinsics.DisableSSE,
HwIntrinsics.DisableSSE42,
new FakeSerializable());
}
@ -209,90 +189,69 @@ public class FeatureTestRunnerTests
switch (Enum.Parse<HwIntrinsics>(intrinsic))
{
case HwIntrinsics.DisableHWIntrinsic:
Assert.False(Sse.IsSupported);
Assert.False(Sse2.IsSupported);
Assert.False(Aes.IsSupported);
Assert.False(Pclmulqdq.IsSupported);
Assert.False(Sse3.IsSupported);
Assert.False(Ssse3.IsSupported);
Assert.False(Sse41.IsSupported);
Assert.False(Sse42.IsSupported);
Assert.False(Popcnt.IsSupported);
Assert.False(Avx.IsSupported);
Assert.False(Fma.IsSupported);
Assert.False(Avx2.IsSupported);
Assert.False(Bmi1.IsSupported);
Assert.False(Bmi2.IsSupported);
Assert.False(Lzcnt.IsSupported);
Assert.False(AdvSimd.IsSupported);
Assert.False(System.Runtime.Intrinsics.Arm.Aes.IsSupported);
Assert.False(Crc32.IsSupported);
Assert.False(Dp.IsSupported);
Assert.False(Sha1.IsSupported);
Assert.False(Sha256.IsSupported);
break;
case HwIntrinsics.DisableSSE:
Assert.False(Sse.IsSupported);
break;
case HwIntrinsics.DisableSSE2:
Assert.False(Sse2.IsSupported);
Assert.False(Sse.IsSupported, "SSE should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse2.IsSupported, "SSE2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Aes.IsSupported, "AES (x86) should be disabled when DisableHWIntrinsic is set.");
Assert.False(Pclmulqdq.IsSupported, "PCLMULQDQ should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse3.IsSupported, "SSE3 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Ssse3.IsSupported, "SSSE3 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse41.IsSupported, "SSE4.1 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sse42.IsSupported, "SSE4.2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Popcnt.IsSupported, "POPCNT should be disabled when DisableHWIntrinsic is set.");
Assert.False(Avx.IsSupported, "AVX should be disabled when DisableHWIntrinsic is set.");
Assert.False(Fma.IsSupported, "FMA should be disabled when DisableHWIntrinsic is set.");
Assert.False(Avx2.IsSupported, "AVX2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Bmi1.IsSupported, "BMI1 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Bmi2.IsSupported, "BMI2 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Lzcnt.IsSupported, "LZCNT should be disabled when DisableHWIntrinsic is set.");
Assert.False(AdvSimd.IsSupported, "Arm64 AdvSimd should be disabled when DisableHWIntrinsic is set.");
Assert.False(System.Runtime.Intrinsics.Arm.Aes.IsSupported, "Arm64 AES should be disabled when DisableHWIntrinsic is set.");
Assert.False(Crc32.IsSupported, "Arm64 CRC32 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Dp.IsSupported, "Arm64 DotProd (DP) should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sha1.IsSupported, "Arm64 SHA1 should be disabled when DisableHWIntrinsic is set.");
Assert.False(Sha256.IsSupported, "Arm64 SHA256 should be disabled when DisableHWIntrinsic is set.");
break;
case HwIntrinsics.DisableAES:
Assert.False(Aes.IsSupported);
break;
case HwIntrinsics.DisablePCLMULQDQ:
Assert.False(Pclmulqdq.IsSupported);
break;
case HwIntrinsics.DisableSSE3:
Assert.False(Sse3.IsSupported);
break;
case HwIntrinsics.DisableSSSE3:
Assert.False(Ssse3.IsSupported);
break;
case HwIntrinsics.DisableSSE41:
Assert.False(Sse41.IsSupported);
Assert.False(Aes.IsSupported, "AES (x86) should be disabled when DisableAES is set.");
#if NET10_0_OR_GREATER
Assert.False(Pclmulqdq.IsSupported, "PCLMULQDQ should be disabled when DisableAES is set (paired disable).");
#endif
break;
case HwIntrinsics.DisableSSE42:
Assert.False(Sse42.IsSupported);
break;
case HwIntrinsics.DisablePOPCNT:
Assert.False(Popcnt.IsSupported);
#if NET10_0_OR_GREATER
Assert.False(Sse3.IsSupported, "Sse3 should be disabled.");
Assert.False(Ssse3.IsSupported, "Ssse3 should be disabled.");
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;
case HwIntrinsics.DisableAVX:
Assert.False(Avx.IsSupported);
break;
case HwIntrinsics.DisableFMA:
Assert.False(Fma.IsSupported);
Assert.False(Avx.IsSupported, "AVX should be disabled when DisableAVX is set.");
break;
case HwIntrinsics.DisableAVX2:
Assert.False(Avx2.IsSupported);
break;
case HwIntrinsics.DisableBMI1:
Assert.False(Bmi1.IsSupported);
break;
case HwIntrinsics.DisableBMI2:
Assert.False(Bmi2.IsSupported);
break;
case HwIntrinsics.DisableLZCNT:
Assert.False(Lzcnt.IsSupported);
break;
case HwIntrinsics.DisableArm64AdvSimd:
Assert.False(AdvSimd.IsSupported);
Assert.False(Avx2.IsSupported, "AVX2 should be disabled when DisableAVX2 is set.");
#if NET10_0_OR_GREATER
Assert.False(Fma.IsSupported, "FMA should be disabled when DisableAVX2 is set (paired disable).");
Assert.False(Bmi1.IsSupported, "BMI1 should be disabled when DisableAVX2 is set (paired disable).");
Assert.False(Bmi2.IsSupported, "BMI2 should be disabled when DisableAVX2 is set (paired disable).");
Assert.False(Lzcnt.IsSupported, "LZCNT should be disabled when DisableAVX2 is set (paired disable).");
#endif
break;
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;
case HwIntrinsics.DisableArm64Crc32:
Assert.False(Crc32.IsSupported);
Assert.False(Crc32.IsSupported, "Arm64 CRC32 should be disabled when DisableArm64Crc32 is set.");
break;
case HwIntrinsics.DisableArm64Dp:
Assert.False(Dp.IsSupported);
Assert.False(Dp.IsSupported, "Arm64 DotProd (DP) should be disabled when DisableArm64Dp is set.");
break;
case HwIntrinsics.DisableArm64Sha1:
Assert.False(Sha1.IsSupported);
Assert.False(Sha1.IsSupported, "Arm64 SHA1 should be disabled when DisableArm64Sha1 is set.");
break;
case HwIntrinsics.DisableArm64Sha256:
Assert.False(Sha256.IsSupported);
Assert.False(Sha256.IsSupported, "Arm64 SHA256 should be disabled when DisableArm64Sha256 is set.");
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
oid sha256:bbe1ffaf7b801fd92724438cc810fd0c5506e0a907b970c4f0bf5bec3627ca2a
size 551
oid sha256:60b050406fda4ff347660e71cb28a9dfceb4b39532f62ee96cb61d2671d3cf00
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
oid sha256:b45933471a1af1b6d4112240e1bc6b6187065a872043ddbf917200ce9e8cc84b
size 371
oid sha256:fbfb3143d96070c58c949e8d1e8d9ddbcf1e7863514489ea2defc65863c84e73
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
oid sha256:b66a5f9d8a7f3f2a78b868bec6c7d1deea927b82d81aa6d1677e0461a3920dc9
size 3800
oid sha256:120b661bef4adac64d362d8c987b3427cd8140ccac7404d09a16765ba1199434
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
oid sha256:d5fdc46ee866e088e0ec3221145a3d2d954a0bcb6d25cbb4d538978272f34949
size 4871
oid sha256:0d668ebe5f8857fd21d7eb9ae86860751a6f3061f6c9f76705ff49216dc07870
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
oid sha256:5ae57ca0658b1ffa7aca9031f4ec065ab5a9813fb8a9c5acd221526df6a4f729
size 9747
oid sha256:2fb676b3af585e7cbe2efdb893157d5f4e152cf810d0693cafb81596e941e121
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
oid sha256:0fced9def2b41cbbf215a49ea6ef6baf4c3c041fd180671eb209db5c6e7177e5
size 10470
oid sha256:afe7ddbff155b918a4eff91af31e01100355c146cb9c8a12ab2496da8b22821d
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
oid sha256:1e4cc16c2f1b439f8780dead04db01fed95f8e20b68270ae8e7a988af999e3db
size 10561
oid sha256:ad76301984e5b54eae374adfe130693667053fbed181847b4c68688fb74c9818
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
oid sha256:06e3966550f1c3ae72796e5522f7829cf1f86daca469c479acf49e6fae72e3d0
size 13227
oid sha256:fbd57af1fa982f9090f57d820a9b927f894914e5f54774e9cd6fdcfe14e5f761
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
oid sha256:8ce5fefe04cc2a036fddcfcf038901a7a09b4ea5d0621a1e0d3abc8430953ae3
size 20778
oid sha256:c4bbc28c203550baf885cefba95c48a3f91dfb5242c09acbf3a8509b7258048e
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
oid sha256:b653c0fe761d351cb15b09f35da578a954d103dea7507e2c1d7c4ebf3bdac49a
size 10943
oid sha256:566e85b1a527f48c953bcc7bc6c58ebd1fe0b14972c38edd596b025e0dd48624
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
oid sha256:3a17bb1653cc6d6ecc292ce0670c651bfea032f61c6a0e84636205bde53a86f8
size 13536
oid sha256:aa5b0d5de93f26c0a7a03b57a00d4a49cda62f4a4b98b6d374261467c03a8357
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
oid sha256:b8970378312c0d479d618e4d5b8da54175c127db517fbe54f9057188d02cc735
size 4165
oid sha256:62267d8d56af3e1452f0e25144f2cfe352b88def98af28e819a3a6982040a4ca
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
oid sha256:3a17bb1653cc6d6ecc292ce0670c651bfea032f61c6a0e84636205bde53a86f8
size 13536
oid sha256:878d5c53b84af4d133825206a327fd4cd02a43831ecabf5c61c5d89181c5a107
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
oid sha256:9bbf7ef00f98b410f309b3bf70ce87d3c6455666a26e89cd004744145a10408a
size 12559
oid sha256:f0aa3c19852632e603ec425aeecc5243d4c6c24a1ac6e3906d29913bf7ead2df
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
oid sha256:7f9ab86abad276d58bb029bd8e2c2aaffac5618322788cb3619577c7643e10d2
size 14223
oid sha256:ec648c2e8006d478ace4a78d2434a4ef7f10d4a3502468cd8b9e2b1f154620b6
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
oid sha256:05c4dc9af1fef422fd5ada2fa1459c26253e0fb5e5a13226fa2e7445ece32272
size 17927
oid sha256:6cb06152d5a0765ad86e8005d6ddac469914ccced89d5ee37d77e7d030b97c9e
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
oid sha256:82b47e1cad2eea417b99a2e4b68a5ba1a6cd6703f360e8402f3dca8b92373ecc
size 18945
oid sha256:38ea8596a682be0075bb31ed176b1fe04b518eb887235d551a574e338d45880b
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
oid sha256:b15ce5a201ee6b946de485a58d3d8e779b6841457e096b2bd7a92968a122f9af
size 20844
oid sha256:965f42f021c63a0f2ccc691723c4ad7f92119294aec407c7ffd46a6238c8f268
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
oid sha256:a1622a48b3f4790d66b229ed29acd18504cedf68d0a548832665c28d47ea663b
size 13857
oid sha256:86f1b9e8f1e38070ce862d87c927313904ceaa9e6080f5acead90e82d164738c
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
oid sha256:74df7b82e2148cfc8dae7e05c96009c0d70c09bf39cdc5ef9d727063d2a8cb3f
size 4154
oid sha256:270f9c2bf5d15fcb21796b3b9eb393e0cc00d9c337036637295ad1efb56781b1
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
oid sha256:cc740ccd76910e384ad84a780591652ac7ee0ea30abf7fd7f5b146f8ff380f07
size 13991
oid sha256:d413162a83c223124a2f29f8154e4bdc08d94bd3e15569ec6cffaa13bdda72c8
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
oid sha256:ccdc54e814604d4d339f6083091abf852aae65052ceb731af998208faddb5b0b
size 13744
oid sha256:941ea7b4d1f2c187f58920546e2f19fc275505929439cc389edcc59e652e8787
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
oid sha256:cd24e0a52c7743ab7d3ed255e3757c2d5495b3f56198556a157df589b1fb67ca
size 14889
oid sha256:cc5e6a607ef2343cb74c5227dbc7861840db956951f1fc4703fe53dbccda0974
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
oid sha256:878f1aab39b0b2405498c24146b8f81248b37b974e5ea7882e96174a034b645f
size 12374
oid sha256:2ac06a9ba2b2c8bef7e0117ac52fbb790101c0f89313dc49feb1f5a1d929ab02
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
oid sha256:dcc2bf4f7e0ab3d56ee71ac1e1855dababeb2e4ec167fd5dc264efdc9e727328
size 17027
oid sha256:ad0f483fa7fda620860858c4f330ba914480fba15d70b408fb1aa3fed52dbfc1
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
oid sha256:6c733878f4c0cc6075a01fbe7cb471f8b3e91c2c5eaf89309ea3c073d9cc4921
size 854
oid sha256:ba501a7fc32a68f8989965aa6457b3860ec42947e2bcd4526c7570ff743f38fc
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
oid sha256:af872886136893938aee82b1ac73e7a1820666a9a5f4bbf34159c09b3283169a
size 5520
oid sha256:8265c5b2e8edd5eaf0aeeccf86cac486e7beec581e696d3b4f4cfee8f4be9b2b
size 5554

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0ba180567e820b145a13c9b26db9c777e95126adfe8e8cacec0ffe1060dcfe8d
size 184124
oid sha256:7f8a4db4facce1d68b363a3b59ea40c9da9fa3c989c736d97a703c84d8230660
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
oid sha256:abce6af307a81a8ebac8e502142b00b2615403b5570c8dbe7b6895cfdd1a6d60
size 66879
oid sha256:ac986987f25d25ab964a5bef710fe81166cb643d85511906218b4f0e72e9e840
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
oid sha256:d4cda265a50aa26711efafdbcd947c9a01eff872611df5298920583f9a3d4224
size 26458
oid sha256:e0ada2a4d32a3a757b803dbf08148f113f5d358b31af79a77e97c660ce96c302
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
oid sha256:278a488a858b8eda141493fe00c617eb1f664196853da8341d7e5b7f231ddce4
size 24645
oid sha256:ffc30373989ec6857797b460931f011b30baaec633b095b6fc3d8fd5d43c77ec
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
oid sha256:e03e79e6fab3a9e43041e54640a04c7cc3677709e7d879f9f410cf8afc7547a7
size 42691
oid sha256:9828ef0faf1a6709673cfe39028ed4202920d346bcc172bda6683bb3d1d0a7a3
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
oid sha256:543dbf5376386bf518830850645d69934e2ca17ab208ce3fd5274a6a172f5206
size 10951
oid sha256:6bff913e6e67129325203fae91278ca17407b10d99c4e4f571e6cfe3b5b7f93c
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
oid sha256:0d0cf291ebf5d8cebab1cd76e2830e5e2d2e0d9a050f7187da72680ead39110c
size 2757
oid sha256:54b761b76d03216e7aa6238eee92755c03f7b016bffd1400be66ecf136b29c26
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
oid sha256:543dbf5376386bf518830850645d69934e2ca17ab208ce3fd5274a6a172f5206
size 10951
oid sha256:16da371a29269dade522b3d602beee8f769723c5712a348d960805b75619376d
size 10889

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

Loading…
Cancel
Save