Browse Source

Merge remote-tracking branch 'upstream/master' into cq10

pull/783/head
Jason Nelson 7 years ago
parent
commit
3a533f48dd
  1. 5
      .github/CONTRIBUTING.md
  2. 4
      src/ImageSharp.Drawing/ImageSharp.Drawing.csproj
  3. 30
      src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs
  4. 2
      src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CIeLchToCieLabConverter.cs
  5. 2
      src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLabToCieLchConverter.cs
  6. 2
      src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLchuvToCieLuvConverter.cs
  7. 2
      src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLuvToCieLchuvConverter.cs
  8. 13
      src/ImageSharp/Common/Helpers/ImageMaths.cs
  9. 101
      src/ImageSharp/Common/Helpers/TolerantMath.cs
  10. 4
      src/ImageSharp/Formats/Png/PngChunkType.cs
  11. 95
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  12. 55
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  13. 32
      src/ImageSharp/Formats/Png/PngMetaData.cs
  14. 16
      src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
  15. 2
      src/ImageSharp/ImageSharp.csproj
  16. 4
      src/ImageSharp/ImageSharp.csproj.DotSettings
  17. 303
      src/ImageSharp/Processing/AffineTransformBuilder.cs
  18. 2
      src/ImageSharp/Processing/KnownFilterMatrices.cs
  19. 211
      src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs
  20. 38
      src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs
  21. 40
      src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs
  22. 117
      src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs
  23. 130
      src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs
  24. 195
      src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs
  25. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/BicubicResampler.cs
  26. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/BoxResampler.cs
  27. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/CatmullRomResampler.cs
  28. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/HermiteResampler.cs
  29. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos2Resampler.cs
  30. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos3Resampler.cs
  31. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos5Resampler.cs
  32. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos8Resampler.cs
  33. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/MitchellNetravaliResampler.cs
  34. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/NearestNeighborResampler.cs
  35. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxResampler.cs
  36. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxSharpResampler.cs
  37. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/SplineResampler.cs
  38. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/TriangleResampler.cs
  39. 0
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/WelchResampler.cs
  40. 85
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs
  41. 81
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs
  42. 247
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs
  43. 22
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs
  44. 20
      src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs
  45. 16
      src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs
  46. 158
      src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs
  47. 161
      src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs
  48. 2
      src/ImageSharp/Processing/Processors/Transforms/TransformProcessorBase.cs
  49. 58
      src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs
  50. 332
      src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs
  51. 319
      src/ImageSharp/Processing/ProjectiveTransformBuilder.cs
  52. 166
      src/ImageSharp/Processing/ProjectiveTransformHelper.cs
  53. 26
      src/ImageSharp/Processing/TaperCorner.cs
  54. 31
      src/ImageSharp/Processing/TaperSide.cs
  55. 141
      src/ImageSharp/Processing/TransformExtensions.cs
  56. 45
      tests/ImageSharp.Benchmarks/Samplers/Rotate.cs
  57. 45
      tests/ImageSharp.Benchmarks/Samplers/Skew.cs
  58. 4
      tests/ImageSharp.Sandbox46/Program.cs
  59. 5
      tests/ImageSharp.Tests/Advanced/AotCompilerTests.cs
  60. 22
      tests/ImageSharp.Tests/Drawing/DrawImageTest.cs
  61. 3
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  62. 2
      tests/ImageSharp.Tests/Formats/Png/PngChunkTypeTests.cs
  63. 2
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs
  64. 67
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  65. 3
      tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs
  66. 168
      tests/ImageSharp.Tests/Helpers/TolerantMathTests.cs
  67. 61
      tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs
  68. 111
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs
  69. 241
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs
  70. 9
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs
  71. 14
      tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs
  72. 71
      tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs
  73. 72
      tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs
  74. 62
      tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs
  75. 27
      tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs
  76. 275
      tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs
  77. 2
      tests/ImageSharp.Tests/Processing/Transforms/TransformsHelpersTest.cs
  78. 4
      tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs
  79. 4
      tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveProfilingBenchmarks.cs
  80. 11
      tests/ImageSharp.Tests/ProfilingBenchmarks/ResizeProfilingBenchmarks.cs
  81. 3
      tests/ImageSharp.Tests/TestImages.cs
  82. 13
      tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs
  83. 13
      tests/ImageSharp.Tests/TestUtilities/TestUtils.cs
  84. 2
      tests/Images/External
  85. BIN
      tests/Images/Input/Png/gray-2-tRNS.png
  86. BIN
      tests/Images/Input/Png/gray-4-tRNS.png
  87. BIN
      tests/Images/Input/Png/gray-8-tRNS.png

5
.github/CONTRIBUTING.md

@ -20,6 +20,11 @@
* Do not open an issue on GitHub until you have collected positive feedback about the change. GitHub issues are primarily intended for bug reports and fixes.
#### **Running tests and Debugging**
* Expected test output is pulled in as a submodule from the [ImageSharp.Tests.Images repository](https://github.com/SixLabors/Imagesharp.Tests.Images/tree/master/ReferenceOutput). To succesfully run tests, make sure that you have updated the submodules!
* Debugging (running tests in Debug mode) is only supported on .NET Core 2.1, because of JIT Code Generation bugs like [dotnet/coreclr#16443](https://github.com/dotnet/coreclr/issues/16443) or [dotnet/coreclr#20657](https://github.com/dotnet/coreclr/issues/20657)
#### **Do you have questions about consuming the library or the source code?**
* Ask any question about how to use ImageSharp in the [ImageSharp Gitter Chat Room](https://gitter.im/ImageSharp/General).

4
src/ImageSharp.Drawing/ImageSharp.Drawing.csproj

@ -38,8 +38,8 @@
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" />
<PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta0007" />
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0007" />
<PackageReference Include="SixLabors.Fonts" Version="1.0.0-dev000119" />
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-dev000102" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta007">
<PrivateAssets>All</PrivateAssets>
</PackageReference>

30
src/ImageSharp.Drawing/Processing/Processors/Drawing/FillRegionProcessor.cs

@ -3,7 +3,7 @@
using System;
using System.Buffers;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Primitives;
@ -107,6 +107,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
Span<float> buffer = bBuffer.GetSpan();
Span<float> scanline = bScanline.GetSpan();
bool isSolidBrushWithoutBlending = this.IsSolidBrushWithoutBlending(out SolidBrush<TPixel> solidBrush);
for (int y = minY; y < maxY; y++)
{
if (scanlineDirty)
@ -168,16 +170,30 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
{
if (!this.Options.Antialias)
{
bool hasOnes = false;
bool hasZeros = false;
for (int x = 0; x < scanlineWidth; x++)
{
if (scanline[x] >= 0.5)
{
scanline[x] = 1;
hasOnes = true;
}
else
{
scanline[x] = 0;
hasZeros = true;
}
}
if (isSolidBrushWithoutBlending && hasOnes != hasZeros)
{
if (hasOnes)
{
source.GetPixelRowSpan(y).Slice(minX, scanlineWidth).Fill(solidBrush.Color);
}
continue;
}
}
@ -187,5 +203,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Drawing
}
}
}
private bool IsSolidBrushWithoutBlending(out SolidBrush<TPixel> solidBrush)
{
solidBrush = this.Brush as SolidBrush<TPixel>;
if (solidBrush == null)
{
return false;
}
return this.Options.IsOpaqueColorWithoutBlending(solidBrush.Color);
}
}
}

2
src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CIeLchToCieLabConverter.cs

@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.ColorSpaces.Conversion.Implementation
// Conversion algorithm described here:
// https://en.wikipedia.org/wiki/Lab_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC
float l = input.L, c = input.C, hDegrees = input.H;
float hRadians = MathFExtensions.DegreeToRadian(hDegrees);
float hRadians = GeometryUtilities.DegreeToRadian(hDegrees);
float a = c * MathF.Cos(hRadians);
float b = c * MathF.Sin(hRadians);

2
src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLabToCieLchConverter.cs

@ -24,7 +24,7 @@ namespace SixLabors.ImageSharp.ColorSpaces.Conversion.Implementation
float l = input.L, a = input.A, b = input.B;
float c = MathF.Sqrt((a * a) + (b * b));
float hRadians = MathF.Atan2(b, a);
float hDegrees = MathFExtensions.RadianToDegree(hRadians);
float hDegrees = GeometryUtilities.RadianToDegree(hRadians);
// Wrap the angle round at 360.
hDegrees %= 360;

2
src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLchuvToCieLuvConverter.cs

@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.ColorSpaces.Conversion.Implementation
// Conversion algorithm described here:
// https://en.wikipedia.org/wiki/CIELUV#Cylindrical_representation_.28CIELCH.29
float l = input.L, c = input.C, hDegrees = input.H;
float hRadians = MathFExtensions.DegreeToRadian(hDegrees);
float hRadians = GeometryUtilities.DegreeToRadian(hDegrees);
float u = c * MathF.Cos(hRadians);
float v = c * MathF.Sin(hRadians);

2
src/ImageSharp/ColorSpaces/Conversion/Implementation/Converters/CieLuvToCieLchuvConverter.cs

@ -24,7 +24,7 @@ namespace SixLabors.ImageSharp.ColorSpaces.Conversion.Implementation
float l = input.L, a = input.U, b = input.V;
float c = MathF.Sqrt((a * a) + (b * b));
float hRadians = MathF.Atan2(b, a);
float hDegrees = MathFExtensions.RadianToDegree(hRadians);
float hDegrees = GeometryUtilities.RadianToDegree(hRadians);
// Wrap the angle round at 360.
hDegrees %= 360;

13
src/ImageSharp/Common/Helpers/ImageMaths.cs

@ -5,6 +5,7 @@ using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp
@ -22,7 +23,8 @@ namespace SixLabors.ImageSharp
/// <param name="b">The blue component.</param>
/// <returns>The <see cref="byte"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static byte Get8BitBT709Luminance(byte r, byte g, byte b) => (byte)((r * .2126F) + (g * .7152F) + (b * .0722F) + 0.5f);
public static byte Get8BitBT709Luminance(byte r, byte g, byte b) =>
(byte)((r * .2126F) + (g * .7152F) + (b * .0722F) + 0.5f);
/// <summary>
/// Gets the luminance from the rgb components using the formula as specified by ITU-R Recommendation BT.709.
@ -32,7 +34,8 @@ namespace SixLabors.ImageSharp
/// <param name="b">The blue component.</param>
/// <returns>The <see cref="ushort"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static ushort Get16BitBT709Luminance(ushort r, ushort g, ushort b) => (ushort)((r * .2126F) + (g * .7152F) + (b * .0722F));
public static ushort Get16BitBT709Luminance(ushort r, ushort g, ushort b) =>
(ushort)((r * .2126F) + (g * .7152F) + (b * .0722F));
/// <summary>
/// Scales a value from a 16 bit <see cref="ushort"/> to it's 8 bit <see cref="byte"/> equivalent.
@ -98,7 +101,6 @@ namespace SixLabors.ImageSharp
/// <summary>
/// Determine the Least Common Multiple (LCM) of two numbers.
/// TODO: This method might be useful for building a more compact <see cref="Processing.Processors.Transforms.KernelMap"/>
/// </summary>
public static int LeastCommonMultiple(int a, int b)
{
@ -123,10 +125,7 @@ namespace SixLabors.ImageSharp
/// <paramref name="m"/> should be power of 2.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public static int ModuloP2(int x, int m)
{
return x & (m - 1);
}
public static int ModuloP2(int x, int m) => x & (m - 1);
/// <summary>
/// Returns the absolute value of a 32-bit signed integer. Uses bit shifting to speed up the operation.

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

@ -0,0 +1,101 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp
{
/// <summary>
/// Implements basic math operations using tolerant comparison
/// whenever an equality check is needed.
/// </summary>
internal readonly struct TolerantMath
{
private readonly double epsilon;
private readonly double negEpsilon;
public TolerantMath(double epsilon)
{
DebugGuard.MustBeGreaterThan(epsilon, 0, nameof(epsilon));
this.epsilon = epsilon;
this.negEpsilon = -epsilon;
}
public static TolerantMath Default { get; } = new TolerantMath(1e-8);
/// <summary>
/// <paramref name="a"/> == 0
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public bool IsZero(double a) => a > this.negEpsilon && a < this.epsilon;
/// <summary>
/// <paramref name="a"/> &gt; 0
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public bool IsPositive(double a) => a > this.epsilon;
/// <summary>
/// <paramref name="a"/> &lt; 0
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public bool IsNegative(double a) => a < this.negEpsilon;
/// <summary>
/// <paramref name="a"/> == <paramref name="b"/>
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public bool AreEqual(double a, double b) => this.IsZero(a - b);
/// <summary>
/// <paramref name="a"/> &gt; <paramref name="b"/>
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public bool IsGreater(double a, double b) => a > b + this.epsilon;
/// <summary>
/// <paramref name="a"/> &lt; <paramref name="b"/>
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public bool IsLess(double a, double b) => a < b - this.epsilon;
/// <summary>
/// <paramref name="a"/> &gt;= <paramref name="b"/>
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public bool IsGreaterOrEqual(double a, double b) => a >= b - this.epsilon;
/// <summary>
/// <paramref name="a"/> &lt;= <paramref name="b"/>
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public bool IsLessOrEqual(double a, double b) => b >= a - this.epsilon;
[MethodImpl(InliningOptions.ShortMethod)]
public double Ceiling(double a)
{
double rem = Math.IEEERemainder(a, 1);
if (this.IsZero(rem))
{
return Math.Round(a);
}
return Math.Ceiling(a);
}
[MethodImpl(InliningOptions.ShortMethod)]
public double Floor(double a)
{
double rem = Math.IEEERemainder(a, 1);
if (this.IsZero(rem))
{
return Math.Round(a);
}
return Math.Floor(a);
}
}
}

4
src/ImageSharp/Formats/Png/PngChunkType.cs

@ -56,10 +56,10 @@ namespace SixLabors.ImageSharp.Formats.Png
Text = 0x74455874U,
/// <summary>
/// This chunk specifies that the image uses simple transparency:
/// The tRNS chunk specifies that the image uses simple transparency:
/// either alpha values associated with palette entries (for indexed-color images)
/// or a single transparent color (for grayscale and true color images).
/// </summary>
PaletteAlpha = 0x74524E53U
Transparency = 0x74524E53U
}
}

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

@ -124,31 +124,6 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
private PngColorType pngColorType;
/// <summary>
/// Represents any color in an 8 bit Rgb24 encoded png that should be transparent
/// </summary>
private Rgb24 rgb24Trans;
/// <summary>
/// Represents any color in a 16 bit Rgb24 encoded png that should be transparent
/// </summary>
private Rgb48 rgb48Trans;
/// <summary>
/// Represents any color in an 8 bit grayscale encoded png that should be transparent
/// </summary>
private byte luminanceTrans;
/// <summary>
/// Represents any color in a 16 bit grayscale encoded png that should be transparent
/// </summary>
private ushort luminance16Trans;
/// <summary>
/// Whether the image has transparency chunk and markers were decoded
/// </summary>
private bool hasTrans;
/// <summary>
/// The next chunk of data to return
/// </summary>
@ -213,7 +188,7 @@ namespace SixLabors.ImageSharp.Formats.Png
using (var deframeStream = new ZlibInflateStream(this.currentStream, this.ReadNextDataChunk))
{
deframeStream.AllocateNewBytes(chunk.Length);
this.ReadScanlines(deframeStream.CompressedStream, image.Frames.RootFrame);
this.ReadScanlines(deframeStream.CompressedStream, image.Frames.RootFrame, pngMetaData);
}
break;
@ -222,11 +197,11 @@ namespace SixLabors.ImageSharp.Formats.Png
Buffer.BlockCopy(chunk.Data.Array, 0, pal, 0, chunk.Length);
this.palette = pal;
break;
case PngChunkType.PaletteAlpha:
case PngChunkType.Transparency:
byte[] alpha = new byte[chunk.Length];
Buffer.BlockCopy(chunk.Data.Array, 0, alpha, 0, chunk.Length);
this.paletteAlpha = alpha;
this.AssignTransparentMarkers(alpha);
this.AssignTransparentMarkers(alpha, pngMetaData);
break;
case PngChunkType.Text:
this.ReadTextChunk(metaData, chunk.Data.Array.AsSpan(0, chunk.Length));
@ -496,16 +471,17 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="dataStream">The <see cref="MemoryStream"/> containing data.</param>
/// <param name="image"> The pixel data.</param>
private void ReadScanlines<TPixel>(Stream dataStream, ImageFrame<TPixel> image)
/// <param name="pngMetaData">The png meta data</param>
private void ReadScanlines<TPixel>(Stream dataStream, ImageFrame<TPixel> image, PngMetaData pngMetaData)
where TPixel : struct, IPixel<TPixel>
{
if (this.header.InterlaceMethod == PngInterlaceMode.Adam7)
{
this.DecodeInterlacedPixelData(dataStream, image);
this.DecodeInterlacedPixelData(dataStream, image, pngMetaData);
}
else
{
this.DecodePixelData(dataStream, image);
this.DecodePixelData(dataStream, image, pngMetaData);
}
}
@ -515,7 +491,8 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="compressedStream">The compressed pixel data stream.</param>
/// <param name="image">The image to decode to.</param>
private void DecodePixelData<TPixel>(Stream compressedStream, ImageFrame<TPixel> image)
/// <param name="pngMetaData">The png meta data</param>
private void DecodePixelData<TPixel>(Stream compressedStream, ImageFrame<TPixel> image, PngMetaData pngMetaData)
where TPixel : struct, IPixel<TPixel>
{
while (this.currentRow < this.header.Height)
@ -555,7 +532,7 @@ namespace SixLabors.ImageSharp.Formats.Png
throw new ImageFormatException("Unknown filter type.");
}
this.ProcessDefilteredScanline(scanlineSpan, image);
this.ProcessDefilteredScanline(scanlineSpan, image, pngMetaData);
this.SwapBuffers();
this.currentRow++;
@ -569,7 +546,8 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="compressedStream">The compressed pixel data stream.</param>
/// <param name="image">The current image.</param>
private void DecodeInterlacedPixelData<TPixel>(Stream compressedStream, ImageFrame<TPixel> image)
/// <param name="pngMetaData">The png meta data</param>
private void DecodeInterlacedPixelData<TPixel>(Stream compressedStream, ImageFrame<TPixel> image, PngMetaData pngMetaData)
where TPixel : struct, IPixel<TPixel>
{
while (true)
@ -626,7 +604,7 @@ namespace SixLabors.ImageSharp.Formats.Png
}
Span<TPixel> rowSpan = image.GetPixelRowSpan(this.currentRow);
this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, Adam7.FirstColumn[this.pass], Adam7.ColumnIncrement[this.pass]);
this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, pngMetaData, Adam7.FirstColumn[this.pass], Adam7.ColumnIncrement[this.pass]);
this.SwapBuffers();
@ -654,7 +632,8 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="defilteredScanline">The de-filtered scanline</param>
/// <param name="pixels">The image</param>
private void ProcessDefilteredScanline<TPixel>(ReadOnlySpan<byte> defilteredScanline, ImageFrame<TPixel> pixels)
/// <param name="pngMetaData">The png meta data</param>
private void ProcessDefilteredScanline<TPixel>(ReadOnlySpan<byte> defilteredScanline, ImageFrame<TPixel> pixels, PngMetaData pngMetaData)
where TPixel : struct, IPixel<TPixel>
{
Span<TPixel> rowSpan = pixels.GetPixelRowSpan(this.currentRow);
@ -674,9 +653,9 @@ namespace SixLabors.ImageSharp.Formats.Png
this.header,
scanlineSpan,
rowSpan,
this.hasTrans,
this.luminance16Trans,
this.luminanceTrans);
pngMetaData.HasTrans,
pngMetaData.TransparentGray16.GetValueOrDefault(),
pngMetaData.TransparentGray8.GetValueOrDefault());
break;
@ -708,9 +687,9 @@ namespace SixLabors.ImageSharp.Formats.Png
rowSpan,
this.bytesPerPixel,
this.bytesPerSample,
this.hasTrans,
this.rgb48Trans,
this.rgb24Trans);
pngMetaData.HasTrans,
pngMetaData.TransparentRgb48.GetValueOrDefault(),
pngMetaData.TransparentRgb24.GetValueOrDefault());
break;
@ -735,9 +714,10 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="defilteredScanline">The de-filtered scanline</param>
/// <param name="rowSpan">The current image row.</param>
/// <param name="pngMetaData">The png meta data</param>
/// <param name="pixelOffset">The column start index. Always 0 for none interlaced images.</param>
/// <param name="increment">The column increment. Always 1 for none interlaced images.</param>
private void ProcessInterlacedDefilteredScanline<TPixel>(ReadOnlySpan<byte> defilteredScanline, Span<TPixel> rowSpan, int pixelOffset = 0, int increment = 1)
private void ProcessInterlacedDefilteredScanline<TPixel>(ReadOnlySpan<byte> defilteredScanline, Span<TPixel> rowSpan, PngMetaData pngMetaData, int pixelOffset = 0, int increment = 1)
where TPixel : struct, IPixel<TPixel>
{
// Trim the first marker byte from the buffer
@ -757,9 +737,9 @@ namespace SixLabors.ImageSharp.Formats.Png
rowSpan,
pixelOffset,
increment,
this.hasTrans,
this.luminance16Trans,
this.luminanceTrans);
pngMetaData.HasTrans,
pngMetaData.TransparentGray16.GetValueOrDefault(),
pngMetaData.TransparentGray8.GetValueOrDefault());
break;
@ -796,9 +776,9 @@ namespace SixLabors.ImageSharp.Formats.Png
increment,
this.bytesPerPixel,
this.bytesPerSample,
this.hasTrans,
this.rgb48Trans,
this.rgb24Trans);
pngMetaData.HasTrans,
pngMetaData.TransparentRgb48.GetValueOrDefault(),
pngMetaData.TransparentRgb24.GetValueOrDefault());
break;
@ -822,7 +802,8 @@ namespace SixLabors.ImageSharp.Formats.Png
/// Decodes and assigns marker colors that identify transparent pixels in non indexed images
/// </summary>
/// <param name="alpha">The alpha tRNS array</param>
private void AssignTransparentMarkers(ReadOnlySpan<byte> alpha)
/// <param name="pngMetaData">The png meta data</param>
private void AssignTransparentMarkers(ReadOnlySpan<byte> alpha, PngMetaData pngMetaData)
{
if (this.pngColorType == PngColorType.Rgb)
{
@ -834,16 +815,16 @@ namespace SixLabors.ImageSharp.Formats.Png
ushort gc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(2, 2));
ushort bc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(4, 2));
this.rgb48Trans = new Rgb48(rc, gc, bc);
this.hasTrans = true;
pngMetaData.TransparentRgb48 = new Rgb48(rc, gc, bc);
pngMetaData.HasTrans = true;
return;
}
byte r = ReadByteLittleEndian(alpha, 0);
byte g = ReadByteLittleEndian(alpha, 2);
byte b = ReadByteLittleEndian(alpha, 4);
this.rgb24Trans = new Rgb24(r, g, b);
this.hasTrans = true;
pngMetaData.TransparentRgb24 = new Rgb24(r, g, b);
pngMetaData.HasTrans = true;
}
}
else if (this.pngColorType == PngColorType.Grayscale)
@ -852,14 +833,14 @@ namespace SixLabors.ImageSharp.Formats.Png
{
if (this.header.BitDepth == 16)
{
this.luminance16Trans = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(0, 2));
pngMetaData.TransparentGray16 = new Gray16(BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(0, 2)));
}
else
{
this.luminanceTrans = ReadByteLittleEndian(alpha, 0);
pngMetaData.TransparentGray8 = new Gray8(ReadByteLittleEndian(alpha, 0));
}
this.hasTrans = true;
pngMetaData.HasTrans = true;
}
}
}

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

@ -290,6 +290,11 @@ namespace SixLabors.ImageSharp.Formats.Png
this.WritePaletteChunk(stream, quantized);
}
if (pngMetaData.HasTrans)
{
this.WriteTransparencyChunk(stream, pngMetaData);
}
this.WritePhysicalChunk(stream, metaData);
this.WriteGammaChunk(stream);
this.WriteExifChunk(stream, metaData);
@ -326,7 +331,6 @@ namespace SixLabors.ImageSharp.Formats.Png
if (this.pngColorType.Equals(PngColorType.Grayscale))
{
// TODO: Research and add support for grayscale plus tRNS
if (this.use16Bit)
{
// 16 bit grayscale
@ -701,7 +705,7 @@ namespace SixLabors.ImageSharp.Formats.Png
// Write the transparency data
if (anyAlpha)
{
this.WriteChunk(stream, PngChunkType.PaletteAlpha, alphaTable.Array, 0, paletteLength);
this.WriteChunk(stream, PngChunkType.Transparency, alphaTable.Array, 0, paletteLength);
}
}
}
@ -749,6 +753,51 @@ namespace SixLabors.ImageSharp.Formats.Png
}
}
/// <summary>
/// Writes the transparency chunk to the stream
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="pngMetaData">The image meta data.</param>
private void WriteTransparencyChunk(Stream stream, PngMetaData pngMetaData)
{
Span<byte> alpha = this.chunkDataBuffer.AsSpan();
if (pngMetaData.ColorType.Equals(PngColorType.Rgb))
{
if (pngMetaData.TransparentRgb48.HasValue && this.use16Bit)
{
Rgb48 rgb = pngMetaData.TransparentRgb48.Value;
BinaryPrimitives.WriteUInt16LittleEndian(alpha, rgb.R);
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G);
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B);
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 6);
}
else if (pngMetaData.TransparentRgb24.HasValue)
{
alpha.Clear();
Rgb24 rgb = pngMetaData.TransparentRgb24.Value;
alpha[1] = rgb.R;
alpha[3] = rgb.G;
alpha[5] = rgb.B;
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 6);
}
}
else if (pngMetaData.ColorType.Equals(PngColorType.Grayscale))
{
if (pngMetaData.TransparentGray16.HasValue && this.use16Bit)
{
BinaryPrimitives.WriteUInt16LittleEndian(alpha, pngMetaData.TransparentGray16.Value.PackedValue);
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 2);
}
else if (pngMetaData.TransparentGray8.HasValue)
{
alpha.Clear();
alpha[1] = pngMetaData.TransparentGray8.Value.PackedValue;
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 2);
}
}
}
/// <summary>
/// Writes the pixel information to the stream.
/// </summary>
@ -947,4 +996,4 @@ namespace SixLabors.ImageSharp.Formats.Png
return scanlineLength / mod;
}
}
}
}

32
src/ImageSharp/Formats/Png/PngMetaData.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png
{
/// <summary>
@ -24,6 +26,11 @@ namespace SixLabors.ImageSharp.Formats.Png
this.BitDepth = other.BitDepth;
this.ColorType = other.ColorType;
this.Gamma = other.Gamma;
this.HasTrans = other.HasTrans;
this.TransparentGray8 = other.TransparentGray8;
this.TransparentGray16 = other.TransparentGray16;
this.TransparentRgb24 = other.TransparentRgb24;
this.TransparentRgb48 = other.TransparentRgb48;
}
/// <summary>
@ -42,6 +49,31 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
public float Gamma { get; set; }
/// <summary>
/// Gets or sets the Rgb 24 transparent color. This represents any color in an 8 bit Rgb24 encoded png that should be transparent
/// </summary>
public Rgb24? TransparentRgb24 { get; set; }
/// <summary>
/// Gets or sets the Rgb 48 transparent color. This represents any color in a 16 bit Rgb24 encoded png that should be transparent
/// </summary>
public Rgb48? TransparentRgb48 { get; set; }
/// <summary>
/// Gets or sets the 8 bit grayscale transparent color. This represents any color in an 8 bit grayscale encoded png that should be transparent
/// </summary>
public Gray8? TransparentGray8 { get; set; }
/// <summary>
/// Gets or sets the 16 bit grayscale transparent color. This represents any color in a 16 bit grayscale encoded png that should be transparent
/// </summary>
public Gray16? TransparentGray16 { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the image has transparency chunk and markers were decoded
/// </summary>
public bool HasTrans { get; set; }
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new PngMetaData(this);
}

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

@ -20,8 +20,8 @@ namespace SixLabors.ImageSharp.Formats.Png
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
bool hasTrans,
ushort luminance16Trans,
byte luminanceTrans)
Gray16 luminance16Trans,
Gray8 luminanceTrans)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
@ -62,7 +62,7 @@ namespace SixLabors.ImageSharp.Formats.Png
rgba64.R = luminance;
rgba64.G = luminance;
rgba64.B = luminance;
rgba64.A = luminance.Equals(luminance16Trans) ? ushort.MinValue : ushort.MaxValue;
rgba64.A = luminance.Equals(luminance16Trans.PackedValue) ? ushort.MinValue : ushort.MaxValue;
pixel.FromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
@ -70,7 +70,7 @@ namespace SixLabors.ImageSharp.Formats.Png
}
else
{
byte scaledLuminanceTrans = (byte)(luminanceTrans * scaleFactor);
byte scaledLuminanceTrans = (byte)(luminanceTrans.PackedValue * scaleFactor);
Rgba32 rgba32 = default;
for (int x = 0; x < header.Width; x++)
{
@ -93,8 +93,8 @@ namespace SixLabors.ImageSharp.Formats.Png
int pixelOffset,
int increment,
bool hasTrans,
ushort luminance16Trans,
byte luminanceTrans)
Gray16 luminance16Trans,
Gray8 luminanceTrans)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
@ -135,7 +135,7 @@ namespace SixLabors.ImageSharp.Formats.Png
rgba64.R = luminance;
rgba64.G = luminance;
rgba64.B = luminance;
rgba64.A = luminance.Equals(luminance16Trans) ? ushort.MinValue : ushort.MaxValue;
rgba64.A = luminance.Equals(luminance16Trans.PackedValue) ? ushort.MinValue : ushort.MaxValue;
pixel.FromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
@ -143,7 +143,7 @@ namespace SixLabors.ImageSharp.Formats.Png
}
else
{
byte scaledLuminanceTrans = (byte)(luminanceTrans * scaleFactor);
byte scaledLuminanceTrans = (byte)(luminanceTrans.PackedValue * scaleFactor);
Rgba32 rgba32 = default;
for (int x = pixelOffset; x < header.Width; x += increment)
{

2
src/ImageSharp/ImageSharp.csproj

@ -42,7 +42,7 @@
<ItemGroup>
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.5.1" />
<PackageReference Include="SixLabors.Core" Version="1.0.0-beta0006" />
<PackageReference Include="SixLabors.Core" Version="1.0.0-dev000089" />
<AdditionalFiles Include="..\..\stylecop.json" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta007">
<PrivateAssets>All</PrivateAssets>

4
src/ImageSharp/ImageSharp.csproj.DotSettings

@ -5,4 +5,6 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=pixelformats_005Cpackedpixels/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=pixelformats_005Cpixelimplementations/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=pixelformats_005Cpixeltypes/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=pixelformats_005Cutils/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=pixelformats_005Cutils/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=processing_005Cprocessors_005Ctransforms_005Cresamplers/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=processing_005Cprocessors_005Ctransforms_005Cresize/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

303
src/ImageSharp/Processing/AffineTransformBuilder.cs

@ -0,0 +1,303 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.Numerics;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing
{
/// <summary>
/// A helper class for constructing <see cref="Matrix3x2"/> instances for use in affine transforms.
/// </summary>
public class AffineTransformBuilder
{
private readonly List<Func<Size, Matrix3x2>> matrixFactories = new List<Func<Size, Matrix3x2>>();
/// <summary>
/// Prepends a rotation matrix using the given rotation angle in degrees
/// and the image center point as rotation center.
/// </summary>
/// <param name="degrees">The amount of rotation, in degrees.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependRotationDegrees(float degrees)
=> this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees));
/// <summary>
/// Prepends a rotation matrix using the given rotation angle in radians
/// and the image center point as rotation center.
/// </summary>
/// <param name="radians">The amount of rotation, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependRotationRadians(float radians)
=> this.Prepend(size => TransformUtils.CreateRotationMatrixRadians(radians, size));
/// <summary>
/// Prepends a rotation matrix using the given rotation in degrees at the given origin.
/// </summary>
/// <param name="degrees">The amount of rotation, in degrees.</param>
/// <param name="origin">The rotation origin point.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependRotationDegrees(float degrees, Vector2 origin)
=> this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees), origin);
/// <summary>
/// Prepends a rotation matrix using the given rotation in radians at the given origin.
/// </summary>
/// <param name="radians">The amount of rotation, in radians.</param>
/// <param name="origin">The rotation origin point.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependRotationRadians(float radians, Vector2 origin)
=> this.PrependMatrix(Matrix3x2.CreateRotation(radians, origin));
/// <summary>
/// Appends a rotation matrix using the given rotation angle in degrees
/// and the image center point as rotation center.
/// </summary>
/// <param name="degrees">The amount of rotation, in degrees.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendRotationDegrees(float degrees)
=> this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees));
/// <summary>
/// Appends a rotation matrix using the given rotation angle in radians
/// and the image center point as rotation center.
/// </summary>
/// <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.CreateRotationMatrixRadians(radians, size));
/// <summary>
/// Appends a rotation matrix using the given rotation in degrees at the given origin.
/// </summary>
/// <param name="degrees">The amount of rotation, in degrees.</param>
/// <param name="origin">The rotation origin point.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendRotationDegrees(float degrees, Vector2 origin)
=> this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees), origin);
/// <summary>
/// Appends a rotation matrix using the given rotation in radians at the given origin.
/// </summary>
/// <param name="radians">The amount of rotation, in radians.</param>
/// <param name="origin">The rotation origin point.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendRotationRadians(float radians, Vector2 origin)
=> this.AppendMatrix(Matrix3x2.CreateRotation(radians, origin));
/// <summary>
/// Prepends a scale matrix from the given uniform scale.
/// </summary>
/// <param name="scale">The uniform scale.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependScale(float scale)
=> this.PrependMatrix(Matrix3x2.CreateScale(scale));
/// <summary>
/// Prepends a scale matrix from the given vector scale.
/// </summary>
/// <param name="scale">The horizontal and vertical scale.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependScale(SizeF scale)
=> this.PrependScale((Vector2)scale);
/// <summary>
/// Prepends a scale matrix from the given vector scale.
/// </summary>
/// <param name="scales">The horizontal and vertical scale.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependScale(Vector2 scales)
=> this.PrependMatrix(Matrix3x2.CreateScale(scales));
/// <summary>
/// Appends a scale matrix from the given uniform scale.
/// </summary>
/// <param name="scale">The uniform scale.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendScale(float scale)
=> this.AppendMatrix(Matrix3x2.CreateScale(scale));
/// <summary>
/// Appends a scale matrix from the given vector scale.
/// </summary>
/// <param name="scales">The horizontal and vertical scale.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendScale(SizeF scales)
=> this.AppendScale((Vector2)scales);
/// <summary>
/// Appends a scale matrix from the given vector scale.
/// </summary>
/// <param name="scales">The horizontal and vertical scale.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendScale(Vector2 scales)
=> this.AppendMatrix(Matrix3x2.CreateScale(scales));
/// <summary>
/// Prepends a centered skew matrix from the give angles in degrees.
/// </summary>
/// <param name="degreesX">The X angle, in degrees.</param>
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY)
=> this.Prepend(size => TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, size));
/// <summary>
/// Prepends a centered skew matrix from the give angles in radians.
/// </summary>
/// <param name="radiansX">The X angle, in radians.</param>
/// <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.CreateSkewMatrixRadians(radiansX, radiansY, size));
/// <summary>
/// Prepends a skew matrix using the given angles in degrees at the given origin.
/// </summary>
/// <param name="degreesX">The X angle, in degrees.</param>
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <param name="origin">The skew origin point.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY, Vector2 origin)
=> this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), origin);
/// <summary>
/// Prepends a skew matrix using the given angles in radians at the given origin.
/// </summary>
/// <param name="radiansX">The X angle, in radians.</param>
/// <param name="radiansY">The Y angle, in radians.</param>
/// <param name="origin">The skew origin point.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY, Vector2 origin)
=> this.PrependMatrix(Matrix3x2.CreateSkew(radiansX, radiansY, origin));
/// <summary>
/// Appends a centered skew matrix from the give angles in degrees.
/// </summary>
/// <param name="degreesX">The X angle, in degrees.</param>
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY)
=> this.Append(size => TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, size));
/// <summary>
/// Appends a centered skew matrix from the give angles in radians.
/// </summary>
/// <param name="radiansX">The X angle, in radians.</param>
/// <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.CreateSkewMatrixRadians(radiansX, radiansY, size));
/// <summary>
/// Appends a skew matrix using the given angles in degrees at the given origin.
/// </summary>
/// <param name="degreesX">The X angle, in degrees.</param>
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <param name="origin">The skew origin point.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY, Vector2 origin)
=> this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), origin);
/// <summary>
/// Appends a skew matrix using the given angles in radians at the given origin.
/// </summary>
/// <param name="radiansX">The X angle, in radians.</param>
/// <param name="radiansY">The Y angle, in radians.</param>
/// <param name="origin">The skew origin point.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY, Vector2 origin)
=> this.AppendMatrix(Matrix3x2.CreateSkew(radiansX, radiansY, origin));
/// <summary>
/// Prepends a translation matrix from the given vector.
/// </summary>
/// <param name="position">The translation position.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependTranslation(PointF position)
=> this.PrependTranslation((Vector2)position);
/// <summary>
/// Prepends a translation matrix from the given vector.
/// </summary>
/// <param name="position">The translation position.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependTranslation(Vector2 position)
=> this.PrependMatrix(Matrix3x2.CreateTranslation(position));
/// <summary>
/// Appends a translation matrix from the given vector.
/// </summary>
/// <param name="position">The translation position.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendTranslation(PointF position)
=> this.AppendTranslation((Vector2)position);
/// <summary>
/// Appends a translation matrix from the given vector.
/// </summary>
/// <param name="position">The translation position.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendTranslation(Vector2 position)
=> this.AppendMatrix(Matrix3x2.CreateTranslation(position));
/// <summary>
/// Prepends a raw matrix.
/// </summary>
/// <param name="matrix">The matrix to prepend.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix) => this.Prepend(_ => matrix);
/// <summary>
/// Appends a raw matrix.
/// </summary>
/// <param name="matrix">The matrix to append.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix) => this.Append(_ => matrix);
/// <summary>
/// Returns the combined matrix for a given source size.
/// </summary>
/// <param name="sourceSize">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
public Matrix3x2 BuildMatrix(Size sourceSize) => this.BuildMatrix(new Rectangle(Point.Empty, sourceSize));
/// <summary>
/// Returns the combined matrix for a given source rectangle.
/// </summary>
/// <param name="sourceRectangle">The rectangle in the source image.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
public Matrix3x2 BuildMatrix(Rectangle sourceRectangle)
{
Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle));
Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle));
// Translate the origin matrix to cater for source rectangle offsets.
var matrix = Matrix3x2.CreateTranslation(-sourceRectangle.Location);
Size size = sourceRectangle.Size;
foreach (Func<Size, Matrix3x2> factory in this.matrixFactories)
{
matrix *= factory(size);
}
return matrix;
}
private AffineTransformBuilder Prepend(Func<Size, Matrix3x2> factory)
{
this.matrixFactories.Insert(0, factory);
return this;
}
private AffineTransformBuilder Append(Func<Size, Matrix3x2> factory)
{
this.matrixFactories.Add(factory);
return this;
}
}
}

2
src/ImageSharp/Processing/KnownFilterMatrices.cs

@ -322,7 +322,7 @@ namespace SixLabors.ImageSharp.Processing
degrees += 360;
}
float radian = MathFExtensions.DegreeToRadian(degrees);
float radian = GeometryUtilities.DegreeToRadian(degrees);
float cosRadian = MathF.Cos(radian);
float sinRadian = MathF.Sin(radian);

211
src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs

@ -5,13 +5,9 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
@ -20,29 +16,35 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// Provides the base methods to perform affine transforms on an image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class AffineTransformProcessor<TPixel> : InterpolatedTransformProcessorBase<TPixel>
internal class AffineTransformProcessor<TPixel> : TransformProcessorBase<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="AffineTransformProcessor{TPixel}"/> class.
/// </summary>
/// <param name="matrix">The transform matrix</param>
/// <param name="matrix">The transform matrix.</param>
/// <param name="sampler">The sampler to perform the transform operation.</param>
/// <param name="targetDimensions">The target dimensions to constrain the transformed image to.</param>
/// <param name="targetDimensions">The target dimensions.</param>
public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size targetDimensions)
: base(sampler)
{
Guard.NotNull(sampler, nameof(sampler));
this.Sampler = sampler;
this.TransformMatrix = matrix;
this.TargetDimensions = targetDimensions;
}
/// <summary>
/// Gets the matrix used to supply the affine transform
/// Gets the sampler to perform interpolation of the transform operation.
/// </summary>
public IResampler Sampler { get; }
/// <summary>
/// Gets the matrix used to supply the affine transform.
/// </summary>
public Matrix3x2 TransformMatrix { get; }
/// <summary>
/// Gets the target dimensions to constrain the transformed image to
/// Gets the target dimensions to constrain the transformed image to.
/// </summary>
public Size TargetDimensions { get; }
@ -64,17 +66,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
Rectangle sourceRectangle,
Configuration configuration)
{
int height = this.TargetDimensions.Height;
int width = this.TargetDimensions.Width;
Rectangle sourceBounds = source.Bounds();
var targetBounds = new Rectangle(0, 0, width, height);
// Handle tranforms that result in output identical to the original.
if (this.TransformMatrix.Equals(default) || this.TransformMatrix.Equals(Matrix3x2.Identity))
{
// The clone will be blank here copy all the pixel data over
source.GetPixelSpan().CopyTo(destination.GetPixelSpan());
return;
}
// Since could potentially be resizing the canvas we might need to re-calculate the matrix
Matrix3x2 matrix = this.GetProcessingMatrix(sourceBounds, targetBounds);
int width = this.TargetDimensions.Width;
var targetBounds = new Rectangle(Point.Empty, this.TargetDimensions);
// Convert from screen to world space.
Matrix3x2.Invert(matrix, out matrix);
Matrix3x2.Invert(this.TransformMatrix, out Matrix3x2 matrix);
if (this.Sampler is NearestNeighborResampler)
{
@ -82,158 +86,57 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
targetBounds,
configuration,
rows =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
for (int x = 0; x < width; x++)
{
var point = Point.Transform(new Point(x, y), matrix);
if (sourceRectangle.Contains(point.X, point.Y))
{
var point = Point.Transform(new Point(x, y), matrix);
if (sourceBounds.Contains(point.X, point.Y))
{
destRow[x] = source[point.X, point.Y];
}
destRow[x] = source[point.X, point.Y];
}
}
});
}
});
return;
}
int maxSourceX = source.Width - 1;
int maxSourceY = source.Height - 1;
(float radius, float scale, float ratio) xRadiusScale = this.GetSamplingRadius(source.Width, destination.Width);
(float radius, float scale, float ratio) yRadiusScale = this.GetSamplingRadius(source.Height, destination.Height);
float xScale = xRadiusScale.scale;
float yScale = yRadiusScale.scale;
var radius = new Vector2(xRadiusScale.radius, yRadiusScale.radius);
IResampler sampler = this.Sampler;
var maxSource = new Vector4(maxSourceX, maxSourceY, maxSourceX, maxSourceY);
int xLength = (int)MathF.Ceiling((radius.X * 2) + 2);
int yLength = (int)MathF.Ceiling((radius.Y * 2) + 2);
MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
using (Buffer2D<float> yBuffer = memoryAllocator.Allocate2D<float>(yLength, height))
using (Buffer2D<float> xBuffer = memoryAllocator.Allocate2D<float>(xLength, height))
var kernel = new TransformKernelMap(configuration, source.Size(), destination.Size(), this.Sampler);
try
{
ParallelHelper.IterateRows(
ParallelHelper.IterateRowsWithTempBuffer<Vector4>(
targetBounds,
configuration,
rows =>
(rows, vectorBuffer) =>
{
Span<Vector4> vectorSpan = vectorBuffer.Span;
for (int y = rows.Min; y < rows.Max; y++)
{
for (int y = rows.Min; y < rows.Max; y++)
{
ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y));
ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y));
ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y));
Span<TPixel> targetRowSpan = destination.GetPixelRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(configuration, targetRowSpan, vectorSpan);
ref float ySpanRef = ref kernel.GetYStartReference(y);
ref float xSpanRef = ref kernel.GetXStartReference(y);
for (int x = 0; x < width; x++)
{
// Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds.
var point = Vector2.Transform(new Vector2(x, y), matrix);
// Clamp sampling pixel radial extents to the source image edges
Vector2 maxXY = point + radius;
Vector2 minXY = point - radius;
// max, maxY, minX, minY
var extents = new Vector4(
MathF.Floor(maxXY.X + .5F),
MathF.Floor(maxXY.Y + .5F),
MathF.Ceiling(minXY.X - .5F),
MathF.Ceiling(minXY.Y - .5F));
int right = (int)extents.X;
int bottom = (int)extents.Y;
int left = (int)extents.Z;
int top = (int)extents.W;
extents = Vector4.Clamp(extents, Vector4.Zero, maxSource);
int maxX = (int)extents.X;
int maxY = (int)extents.Y;
int minX = (int)extents.Z;
int minY = (int)extents.W;
if (minX == maxX || minY == maxY)
{
continue;
}
// It appears these have to be calculated on-the-fly.
// Precalculating transformed weights would require prior knowledge of every transformed pixel location
// since they can be at sub-pixel positions on both axis.
// I've optimized where I can but am always open to suggestions.
if (yScale > 1 && xScale > 1)
{
CalculateWeightsDown(
top,
bottom,
minY,
maxY,
point.Y,
sampler,
yScale,
ref ySpanRef,
yLength);
CalculateWeightsDown(
left,
right,
minX,
maxX,
point.X,
sampler,
xScale,
ref xSpanRef,
xLength);
}
else
{
CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef);
CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef);
}
// Now multiply the results against the offsets
Vector4 sum = Vector4.Zero;
for (int yy = 0, j = minY; j <= maxY; j++, yy++)
{
float yWeight = Unsafe.Add(ref ySpanRef, yy);
for (int xx = 0, i = minX; i <= maxX; i++, xx++)
{
float xWeight = Unsafe.Add(ref xSpanRef, xx);
// Values are first premultiplied to prevent darkening of edge pixels
var current = source[i, j].ToVector4();
Vector4Utils.Premultiply(ref current);
sum += current * xWeight * yWeight;
}
}
ref TPixel dest = ref Unsafe.Add(ref destRowRef, x);
// Reverse the premultiplication
Vector4Utils.UnPremultiply(ref sum);
dest.FromVector4(sum);
}
for (int x = 0; x < width; x++)
{
// Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds.
var point = Vector2.Transform(new Vector2(x, y), matrix);
kernel.Convolve(point, x, ref ySpanRef, ref xSpanRef, source.PixelBuffer, vectorSpan);
}
});
PixelOperations<TPixel>.Instance.FromVector4(configuration, vectorSpan, targetRowSpan);
}
});
}
finally
{
kernel.Dispose();
}
}
/// <summary>
/// Gets a transform matrix adjusted for final processing based upon the target image bounds.
/// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="destinationRectangle">The destination image bounds.</param>
/// <returns>
/// The <see cref="Matrix3x2"/>.
/// </returns>
protected virtual Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle)
=> this.TransformMatrix;
}
}

38
src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs

@ -1,38 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// A base class that provides methods to allow the automatic centering of affine transforms
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal abstract class CenteredAffineTransformProcessor<TPixel> : AffineTransformProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="CenteredAffineTransformProcessor{TPixel}"/> class.
/// </summary>
/// <param name="matrix">The transform matrix</param>
/// <param name="sampler">The sampler to perform the transform operation.</param>
/// <param name="sourceSize">The source image size</param>
protected CenteredAffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size sourceSize)
: base(matrix, sampler, GetTransformedDimensions(sourceSize, matrix))
{
}
/// <inheritdoc/>
protected override Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle)
=> TransformHelpers.GetCenteredTransformMatrix(sourceRectangle, destinationRectangle, this.TransformMatrix);
private static Size GetTransformedDimensions(Size sourceDimensions, Matrix3x2 matrix)
{
var sourceRectangle = new Rectangle(0, 0, sourceDimensions.Width, sourceDimensions.Height);
return TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, matrix).Size;
}
}
}

40
src/ImageSharp/Processing/Processors/Transforms/CenteredProjectiveTransformProcessor.cs

@ -1,40 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// A base class that provides methods to allow the automatic centering of non-affine transforms
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal abstract class CenteredProjectiveTransformProcessor<TPixel> : ProjectiveTransformProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="CenteredProjectiveTransformProcessor{TPixel}"/> class.
/// </summary>
/// <param name="matrix">The transform matrix</param>
/// <param name="sampler">The sampler to perform the transform operation.</param>
/// <param name="sourceSize">The source image size</param>
protected CenteredProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Size sourceSize)
: base(matrix, sampler, GetTransformedDimensions(sourceSize, matrix))
{
}
/// <inheritdoc/>
protected override Matrix4x4 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle)
{
return TransformHelpers.GetCenteredTransformMatrix(sourceRectangle, destinationRectangle, this.TransformMatrix);
}
private static Size GetTransformedDimensions(Size sourceDimensions, Matrix4x4 matrix)
{
var sourceRectangle = new Rectangle(0, 0, sourceDimensions.Width, sourceDimensions.Height);
return TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, matrix).Size;
}
}
}

117
src/ImageSharp/Processing/Processors/Transforms/InterpolatedTransformProcessorBase.cs

@ -1,117 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// The base class for performing interpolated affine and non-affine transforms.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal abstract class InterpolatedTransformProcessorBase<TPixel> : TransformProcessorBase<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="InterpolatedTransformProcessorBase{TPixel}"/> class.
/// </summary>
/// <param name="sampler">The sampler to perform the transform operation.</param>
protected InterpolatedTransformProcessorBase(IResampler sampler)
{
Guard.NotNull(sampler, nameof(sampler));
this.Sampler = sampler;
}
/// <summary>
/// Gets the sampler to perform interpolation of the transform operation.
/// </summary>
public IResampler Sampler { get; }
/// <summary>
/// Calculated the weights for the given point.
/// This method uses more samples than the upscaled version to ensure edge pixels are correctly rendered.
/// Additionally the weights are normalized.
/// </summary>
/// <param name="min">The minimum sampling offset</param>
/// <param name="max">The maximum sampling offset</param>
/// <param name="sourceMin">The minimum source bounds</param>
/// <param name="sourceMax">The maximum source bounds</param>
/// <param name="point">The transformed point dimension</param>
/// <param name="sampler">The sampler</param>
/// <param name="scale">The transformed image scale relative to the source</param>
/// <param name="weightsRef">The reference to the collection of weights</param>
/// <param name="length">The length of the weights collection</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected static void CalculateWeightsDown(int min, int max, int sourceMin, int sourceMax, float point, IResampler sampler, float scale, ref float weightsRef, int length)
{
float sum = 0;
// Downsampling weights requires more edge sampling plus normalization of the weights
for (int x = 0, i = min; i <= max; i++, x++)
{
int index = i;
if (index < sourceMin)
{
index = sourceMin;
}
if (index > sourceMax)
{
index = sourceMax;
}
float weight = sampler.GetValue((index - point) / scale);
sum += weight;
Unsafe.Add(ref weightsRef, x) = weight;
}
if (sum > 0)
{
for (int i = 0; i < length; i++)
{
ref float wRef = ref Unsafe.Add(ref weightsRef, i);
wRef = wRef / sum;
}
}
}
/// <summary>
/// Calculated the weights for the given point.
/// </summary>
/// <param name="sourceMin">The minimum source bounds</param>
/// <param name="sourceMax">The maximum source bounds</param>
/// <param name="point">The transformed point dimension</param>
/// <param name="sampler">The sampler</param>
/// <param name="weightsRef">The reference to the collection of weights</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected static void CalculateWeightsScaleUp(int sourceMin, int sourceMax, float point, IResampler sampler, ref float weightsRef)
{
for (int x = 0, i = sourceMin; i <= sourceMax; i++, x++)
{
float weight = sampler.GetValue(i - point);
Unsafe.Add(ref weightsRef, x) = weight;
}
}
/// <summary>
/// Calculates the sampling radius for the current sampler
/// </summary>
/// <param name="sourceSize">The source dimension size</param>
/// <param name="destinationSize">The destination dimension size</param>
/// <returns>The radius, and scaling factor</returns>
protected (float radius, float scale, float ratio) GetSamplingRadius(int sourceSize, int destinationSize)
{
float ratio = (float)sourceSize / destinationSize;
float scale = ratio;
if (scale < 1F)
{
scale = 1F;
}
return (MathF.Ceiling(scale * this.Sampler.Radius), scale, ratio);
}
}
}

130
src/ImageSharp/Processing/Processors/Transforms/KernelMap.cs

@ -1,130 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.Memory;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Holds the <see cref="ResizeKernel"/> values in an optimized contigous memory region.
/// </summary>
internal class KernelMap : IDisposable
{
private readonly Buffer2D<float> data;
/// <summary>
/// Initializes a new instance of the <see cref="KernelMap"/> class.
/// </summary>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for allocations.</param>
/// <param name="destinationSize">The size of the destination window</param>
/// <param name="kernelRadius">The radius of the kernel</param>
public KernelMap(MemoryAllocator memoryAllocator, int destinationSize, float kernelRadius)
{
int width = (int)Math.Ceiling(kernelRadius * 2);
this.data = memoryAllocator.Allocate2D<float>(width, destinationSize, AllocationOptions.Clean);
this.Kernels = new ResizeKernel[destinationSize];
}
/// <summary>
/// Gets the calculated <see cref="Kernels"/> values.
/// </summary>
public ResizeKernel[] Kernels { get; }
/// <summary>
/// Disposes <see cref="KernelMap"/> instance releasing it's backing buffer.
/// </summary>
public void Dispose()
{
this.data.Dispose();
}
/// <summary>
/// Computes the weights to apply at each pixel when resizing.
/// </summary>
/// <param name="sampler">The <see cref="IResampler"/></param>
/// <param name="destinationSize">The destination size</param>
/// <param name="sourceSize">The source size</param>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for buffer allocations</param>
/// <returns>The <see cref="KernelMap"/></returns>
public static KernelMap Calculate(
IResampler sampler,
int destinationSize,
int sourceSize,
MemoryAllocator memoryAllocator)
{
float ratio = (float)sourceSize / destinationSize;
float scale = ratio;
if (scale < 1F)
{
scale = 1F;
}
float radius = MathF.Ceiling(scale * sampler.Radius);
var result = new KernelMap(memoryAllocator, destinationSize, radius);
for (int i = 0; i < destinationSize; i++)
{
float center = ((i + .5F) * ratio) - .5F;
// Keep inside bounds.
int left = (int)MathF.Ceiling(center - radius);
if (left < 0)
{
left = 0;
}
int right = (int)MathF.Floor(center + radius);
if (right > sourceSize - 1)
{
right = sourceSize - 1;
}
float sum = 0;
ResizeKernel ws = result.CreateKernel(i, left, right);
result.Kernels[i] = ws;
ref float weightsBaseRef = ref ws.GetStartReference();
for (int j = left; j <= right; j++)
{
float weight = sampler.GetValue((j - center) / scale);
sum += weight;
// weights[j - left] = weight:
Unsafe.Add(ref weightsBaseRef, j - left) = weight;
}
// Normalize, best to do it here rather than in the pixel loop later on.
if (sum > 0)
{
for (int w = 0; w < ws.Length; w++)
{
// weights[w] = weights[w] / sum:
ref float wRef = ref Unsafe.Add(ref weightsBaseRef, w);
wRef /= sum;
}
}
}
return result;
}
/// <summary>
/// Slices a weights value at the given positions.
/// </summary>
/// <param name="destIdx">The index in destination buffer</param>
/// <param name="leftIdx">The local left index value</param>
/// <param name="rightIdx">The local right index value</param>
/// <returns>The weights</returns>
private ResizeKernel CreateKernel(int destIdx, int leftIdx, int rightIdx)
{
return new ResizeKernel(destIdx, leftIdx, this.data, rightIdx - leftIdx + 1);
}
}
}

195
src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs

@ -5,13 +5,9 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
@ -20,22 +16,28 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// Provides the base methods to perform non-affine transforms on an image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class ProjectiveTransformProcessor<TPixel> : InterpolatedTransformProcessorBase<TPixel>
internal class ProjectiveTransformProcessor<TPixel> : TransformProcessorBase<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="ProjectiveTransformProcessor{TPixel}"/> class.
/// </summary>
/// <param name="matrix">The transform matrix</param>
/// <param name="matrix">The transform matrix.</param>
/// <param name="sampler">The sampler to perform the transform operation.</param>
/// <param name="targetDimensions">The target dimensions to constrain the transformed image to.</param>
/// <param name="targetDimensions">The target dimensions.</param>
public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Size targetDimensions)
: base(sampler)
{
Guard.NotNull(sampler, nameof(sampler));
this.Sampler = sampler;
this.TransformMatrix = matrix;
this.TargetDimensions = targetDimensions;
}
/// <summary>
/// Gets the sampler to perform interpolation of the transform operation.
/// </summary>
public IResampler Sampler { get; }
/// <summary>
/// Gets the matrix used to supply the projective transform
/// </summary>
@ -60,17 +62,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Rectangle sourceRectangle, Configuration configuration)
{
int height = this.TargetDimensions.Height;
int width = this.TargetDimensions.Width;
Rectangle sourceBounds = source.Bounds();
var targetBounds = new Rectangle(0, 0, width, height);
// Handle tranforms that result in output identical to the original.
if (this.TransformMatrix.Equals(default) || this.TransformMatrix.Equals(Matrix4x4.Identity))
{
// The clone will be blank here copy all the pixel data over
source.GetPixelSpan().CopyTo(destination.GetPixelSpan());
return;
}
// Since could potentially be resizing the canvas we might need to re-calculate the matrix
Matrix4x4 matrix = this.GetProcessingMatrix(sourceBounds, targetBounds);
int width = this.TargetDimensions.Width;
var targetBounds = new Rectangle(Point.Empty, this.TargetDimensions);
// Convert from screen to world space.
Matrix4x4.Invert(matrix, out matrix);
Matrix4x4.Invert(this.TransformMatrix, out Matrix4x4 matrix);
const float Epsilon = 0.0000001F;
if (this.Sampler is NearestNeighborResampler)
@ -92,7 +96,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int px = (int)MathF.Round(v3.X / z);
int py = (int)MathF.Round(v3.Y / z);
if (sourceBounds.Contains(px, py))
if (sourceRectangle.Contains(px, py))
{
destRow[x] = source[px, py];
}
@ -103,145 +107,40 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return;
}
int maxSourceX = source.Width - 1;
int maxSourceY = source.Height - 1;
(float radius, float scale, float ratio) xRadiusScale = this.GetSamplingRadius(source.Width, destination.Width);
(float radius, float scale, float ratio) yRadiusScale = this.GetSamplingRadius(source.Height, destination.Height);
float xScale = xRadiusScale.scale;
float yScale = yRadiusScale.scale;
// Using Vector4 with dummy 0-s, because Vector2 SIMD implementation is not reliable:
var radius = new Vector4(xRadiusScale.radius, yRadiusScale.radius, 0, 0);
IResampler sampler = this.Sampler;
var maxSource = new Vector4(maxSourceX, maxSourceY, maxSourceX, maxSourceY);
int xLength = (int)MathF.Ceiling((radius.X * 2) + 2);
int yLength = (int)MathF.Ceiling((radius.Y * 2) + 2);
MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
using (Buffer2D<float> yBuffer = memoryAllocator.Allocate2D<float>(yLength, height))
using (Buffer2D<float> xBuffer = memoryAllocator.Allocate2D<float>(xLength, height))
var kernel = new TransformKernelMap(configuration, source.Size(), destination.Size(), this.Sampler);
try
{
ParallelHelper.IterateRows(
ParallelHelper.IterateRowsWithTempBuffer<Vector4>(
targetBounds,
configuration,
rows =>
(rows, vectorBuffer) =>
{
Span<Vector4> vectorSpan = vectorBuffer.Span;
for (int y = rows.Min; y < rows.Max; y++)
{
for (int y = rows.Min; y < rows.Max; y++)
{
ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y));
ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y));
ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y));
for (int x = 0; x < width; x++)
{
// Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds.
var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix);
float z = MathF.Max(v3.Z, Epsilon);
// Using Vector4 with dummy 0-s, because Vector2 SIMD implementation is not reliable:
Vector4 point = new Vector4(v3.X, v3.Y, 0, 0) / z;
// Clamp sampling pixel radial extents to the source image edges
Vector4 maxXY = point + radius;
Vector4 minXY = point - radius;
// max, maxY, minX, minY
var extents = new Vector4(
MathF.Floor(maxXY.X + .5F),
MathF.Floor(maxXY.Y + .5F),
MathF.Ceiling(minXY.X - .5F),
MathF.Ceiling(minXY.Y - .5F));
Span<TPixel> targetRowSpan = destination.GetPixelRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(configuration, targetRowSpan, vectorSpan);
ref float ySpanRef = ref kernel.GetYStartReference(y);
ref float xSpanRef = ref kernel.GetXStartReference(y);
int right = (int)extents.X;
int bottom = (int)extents.Y;
int left = (int)extents.Z;
int top = (int)extents.W;
extents = Vector4.Clamp(extents, Vector4.Zero, maxSource);
int maxX = (int)extents.X;
int maxY = (int)extents.Y;
int minX = (int)extents.Z;
int minY = (int)extents.W;
if (minX == maxX || minY == maxY)
{
continue;
}
// It appears these have to be calculated on-the-fly.
// Precalulating transformed weights would require prior knowledge of every transformed pixel location
// since they can be at sub-pixel positions on both axis.
// I've optimized where I can but am always open to suggestions.
if (yScale > 1 && xScale > 1)
{
CalculateWeightsDown(
top,
bottom,
minY,
maxY,
point.Y,
sampler,
yScale,
ref ySpanRef,
yLength);
CalculateWeightsDown(
left,
right,
minX,
maxX,
point.X,
sampler,
xScale,
ref xSpanRef,
xLength);
}
else
{
CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef);
CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef);
}
// Now multiply the results against the offsets
Vector4 sum = Vector4.Zero;
for (int yy = 0, j = minY; j <= maxY; j++, yy++)
{
float yWeight = Unsafe.Add(ref ySpanRef, yy);
for (int xx = 0, i = minX; i <= maxX; i++, xx++)
{
float xWeight = Unsafe.Add(ref xSpanRef, xx);
// Values are first premultiplied to prevent darkening of edge pixels
var current = source[i, j].ToVector4();
Vector4Utils.Premultiply(ref current);
sum += current * xWeight * yWeight;
}
}
ref TPixel dest = ref Unsafe.Add(ref destRowRef, x);
for (int x = 0; x < width; x++)
{
// Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds.
var v3 = Vector3.Transform(new Vector3(x, y, 1F), matrix);
Vector2 point = new Vector2(v3.X, v3.Y) / MathF.Max(v3.Z, Epsilon);
// Reverse the premultiplication
Vector4Utils.UnPremultiply(ref sum);
dest.FromVector4(sum);
}
kernel.Convolve(point, x, ref ySpanRef, ref xSpanRef, source.PixelBuffer, vectorSpan);
}
});
PixelOperations<TPixel>.Instance.FromVector4(configuration, vectorSpan, targetRowSpan);
}
});
}
finally
{
kernel.Dispose();
}
}
/// <summary>
/// Gets a transform matrix adjusted for final processing based upon the target image bounds.
/// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="destinationRectangle">The destination image bounds.</param>
/// <returns>
/// The <see cref="Matrix4x4"/>.
/// </returns>
protected virtual Matrix4x4 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) => this.TransformMatrix;
}
}

0
src/ImageSharp/Processing/Processors/Transforms/BicubicResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/BicubicResampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/BoxResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/BoxResampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/CatmullRomResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/CatmullRomResampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/HermiteResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/HermiteResampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/Lanczos2Resampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos2Resampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/Lanczos3Resampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos3Resampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/Lanczos5Resampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos5Resampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/Lanczos8Resampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos8Resampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/MitchellNetravaliResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/MitchellNetravaliResampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/NearestNeighborResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/NearestNeighborResampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/RobidouxResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxResampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/RobidouxSharpResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxSharpResampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/SplineResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/SplineResampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/TriangleResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/TriangleResampler.cs

0
src/ImageSharp/Processing/Processors/Transforms/WelchResampler.cs → src/ImageSharp/Processing/Processors/Transforms/Resamplers/WelchResampler.cs

85
src/ImageSharp/Processing/Processors/Transforms/ResizeKernel.cs → src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs

@ -2,82 +2,62 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.Memory;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Points to a collection of of weights allocated in <see cref="KernelMap"/>.
/// Points to a collection of of weights allocated in <see cref="ResizeKernelMap"/>.
/// </summary>
internal struct ResizeKernel
internal readonly unsafe struct ResizeKernel
{
/// <summary>
/// The local left index position
/// </summary>
public int Left;
/// <summary>
/// The length of the weights window
/// </summary>
public int Length;
/// <summary>
/// The buffer containing the weights values.
/// </summary>
private readonly Memory<float> buffer;
private readonly float* bufferPtr;
/// <summary>
/// Initializes a new instance of the <see cref="ResizeKernel"/> struct.
/// </summary>
/// <param name="index">The destination index in the buffer</param>
/// <param name="left">The local left index</param>
/// <param name="buffer">The span</param>
/// <param name="length">The length of the window</param>
[MethodImpl(InliningOptions.ShortMethod)]
internal ResizeKernel(int index, int left, Buffer2D<float> buffer, int length)
internal ResizeKernel(int left, float* bufferPtr, int length)
{
int flatStartIndex = index * buffer.Width;
this.Left = left;
this.buffer = buffer.MemorySource.Memory.Slice(flatStartIndex, length);
this.bufferPtr = bufferPtr;
this.Length = length;
}
/// <summary>
/// Gets a reference to the first item of the window.
/// Gets the left index for the destination row
/// </summary>
/// <returns>The reference to the first item of the window</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public ref float GetStartReference()
{
Span<float> span = this.buffer.Span;
return ref span[0];
}
public int Left { get; }
/// <summary>
/// Gets the span representing the portion of the <see cref="KernelMap"/> that this window covers
/// Gets the the length of the kernel
/// </summary>
/// <returns>The <see cref="Span{T}"/></returns>
[MethodImpl(InliningOptions.ShortMethod)]
public Span<float> GetSpan() => this.buffer.Span;
public int Length { get; }
/// <summary>
/// Gets the span representing the portion of the <see cref="ResizeKernelMap"/> that this window covers
/// </summary>
/// <value>The <see cref="Span{T}"/>
/// </value>
public Span<float> Values
{
[MethodImpl(InliningOptions.ShortMethod)]
get => new Span<float>(this.bufferPtr, this.Length);
}
/// <summary>
/// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this <see cref="ResizeKernel"/> instance.
/// </summary>
/// <param name="rowSpan">The input span of vectors</param>
/// <param name="sourceX">The source row position.</param>
/// <returns>The weighted sum</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public Vector4 Convolve(Span<Vector4> rowSpan, int sourceX)
public Vector4 Convolve(Span<Vector4> rowSpan)
{
ref float horizontalValues = ref this.GetStartReference();
ref float horizontalValues = ref Unsafe.AsRef<float>(this.bufferPtr);
int left = this.Left;
ref Vector4 vecPtr = ref Unsafe.Add(ref MemoryMarshal.GetReference(rowSpan), left + sourceX);
ref Vector4 vecPtr = ref Unsafe.Add(ref MemoryMarshal.GetReference(rowSpan), left);
// Destination color components
Vector4 result = Vector4.Zero;
@ -91,5 +71,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return result;
}
/// <summary>
/// Copy the contents of <see cref="ResizeKernel"/> altering <see cref="Left"/>
/// to the value <paramref name="left"/>.
/// </summary>
internal ResizeKernel AlterLeftValue(int left)
{
return new ResizeKernel(left, this.bufferPtr, this.Length);
}
internal void Fill(Span<double> values)
{
DebugGuard.IsTrue(values.Length == this.Length, nameof(values), "ResizeKernel.Fill: values.Length != this.Length!");
for (int i = 0; i < this.Length; i++)
{
this.Values[i] = (float)values[i];
}
}
}
}

81
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs

@ -0,0 +1,81 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.Memory;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <content>
/// Contains <see cref="PeriodicKernelMap"/>
/// </content>
internal partial class ResizeKernelMap
{
/// <summary>
/// Memory-optimized <see cref="ResizeKernelMap"/> where repeating rows are stored only once.
/// </summary>
private sealed class PeriodicKernelMap : ResizeKernelMap
{
private readonly int period;
private readonly int cornerInterval;
public PeriodicKernelMap(
MemoryAllocator memoryAllocator,
IResampler sampler,
int sourceLength,
int destinationLength,
double ratio,
double scale,
int radius,
int period,
int cornerInterval)
: base(
memoryAllocator,
sampler,
sourceLength,
destinationLength,
(cornerInterval * 2) + period,
ratio,
scale,
radius)
{
this.cornerInterval = cornerInterval;
this.period = period;
}
internal override string Info => base.Info + $"|period:{this.period}|cornerInterval:{this.cornerInterval}";
protected override void Initialize()
{
// Build top corner data + one period of the mosaic data:
int startOfFirstRepeatedMosaic = this.cornerInterval + this.period;
for (int i = 0; i < startOfFirstRepeatedMosaic; i++)
{
ResizeKernel kernel = this.BuildKernel(i, i);
this.kernels[i] = kernel;
}
// Copy the mosaics:
int bottomStartDest = this.DestinationLength - this.cornerInterval;
for (int i = startOfFirstRepeatedMosaic; i < bottomStartDest; i++)
{
double center = ((i + .5) * this.ratio) - .5;
int left = (int)TolerantMath.Ceiling(center - this.radius);
ResizeKernel kernel = this.kernels[i - this.period];
this.kernels[i] = kernel.AlterLeftValue(left);
}
// Build bottom corner data:
int bottomStartData = this.cornerInterval + this.period;
for (int i = 0; i < this.cornerInterval; i++)
{
ResizeKernel kernel = this.BuildKernel(bottomStartDest + i, bottomStartData + i);
this.kernels[bottomStartDest + i] = kernel;
}
}
}
}
}

247
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs

@ -0,0 +1,247 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.Memory;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Provides <see cref="ResizeKernel"/> values from an optimized,
/// contiguous memory region.
/// </summary>
internal partial class ResizeKernelMap : IDisposable
{
private static readonly TolerantMath TolerantMath = TolerantMath.Default;
private readonly IResampler sampler;
private readonly int sourceLength;
private readonly double ratio;
private readonly double scale;
private readonly int radius;
private readonly MemoryHandle pinHandle;
private readonly Buffer2D<float> data;
private readonly ResizeKernel[] kernels;
// To avoid both GC allocations, and MemoryAllocator ceremony:
private readonly double[] tempValues;
private ResizeKernelMap(
MemoryAllocator memoryAllocator,
IResampler sampler,
int sourceLength,
int destinationLength,
int bufferHeight,
double ratio,
double scale,
int radius)
{
this.sampler = sampler;
this.ratio = ratio;
this.scale = scale;
this.radius = radius;
this.sourceLength = sourceLength;
this.DestinationLength = destinationLength;
int maxWidth = (radius * 2) + 1;
this.data = memoryAllocator.Allocate2D<float>(maxWidth, bufferHeight, AllocationOptions.Clean);
this.pinHandle = this.data.Memory.Pin();
this.kernels = new ResizeKernel[destinationLength];
this.tempValues = new double[maxWidth];
}
/// <summary>
/// Gets the length of the destination row/column
/// </summary>
public int DestinationLength { get; }
/// <summary>
/// Gets a string of information to help debugging
/// </summary>
internal virtual string Info =>
$"radius:{this.radius}|sourceSize:{this.sourceLength}|destinationSize:{this.DestinationLength}|ratio:{this.ratio}|scale:{this.scale}";
/// <summary>
/// Disposes <see cref="ResizeKernelMap"/> instance releasing it's backing buffer.
/// </summary>
public void Dispose()
{
this.pinHandle.Dispose();
this.data.Dispose();
}
/// <summary>
/// Returns a <see cref="ResizeKernel"/> for an index value between 0 and DestinationSize - 1.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public ref ResizeKernel GetKernel(int destIdx) => ref this.kernels[destIdx];
/// <summary>
/// Computes the weights to apply at each pixel when resizing.
/// </summary>
/// <param name="sampler">The <see cref="IResampler"/></param>
/// <param name="destinationSize">The destination size</param>
/// <param name="sourceSize">The source size</param>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for buffer allocations</param>
/// <returns>The <see cref="ResizeKernelMap"/></returns>
public static ResizeKernelMap Calculate(
IResampler sampler,
int destinationSize,
int sourceSize,
MemoryAllocator memoryAllocator)
{
double ratio = (double)sourceSize / destinationSize;
double scale = ratio;
if (scale < 1)
{
scale = 1;
}
int radius = (int)TolerantMath.Ceiling(scale * sampler.Radius);
// 'ratio' is a rational number.
// Multiplying it by LCM(sourceSize, destSize)/sourceSize will result in a whole number "again".
// This value is determining the length of the periods in repeating kernel map rows.
int period = ImageMaths.LeastCommonMultiple(sourceSize, destinationSize) / sourceSize;
// the center position at i == 0:
double center0 = (ratio - 1) * 0.5;
double firstNonNegativeLeftVal = (radius - center0 - 1) / ratio;
// The number of rows building a "stairway" at the top and the bottom of the kernel map
// corresponding to the corners of the image.
// If we do not normalize the kernel values, these rows also fit the periodic logic,
// however, it's just simpler to calculate them separately.
int cornerInterval = (int)TolerantMath.Ceiling(firstNonNegativeLeftVal);
// If firstNonNegativeLeftVal was an integral value, we need firstNonNegativeLeftVal+1
// instead of Ceiling:
if (TolerantMath.AreEqual(firstNonNegativeLeftVal, cornerInterval))
{
cornerInterval++;
}
// If 'cornerInterval' is too big compared to 'period', we can't apply the periodic optimization.
// If we don't have at least 2 periods, we go with the basic implementation:
bool hasAtLeast2Periods = 2 * (cornerInterval + period) < destinationSize;
ResizeKernelMap result = hasAtLeast2Periods
? new PeriodicKernelMap(
memoryAllocator,
sampler,
sourceSize,
destinationSize,
ratio,
scale,
radius,
period,
cornerInterval)
: new ResizeKernelMap(
memoryAllocator,
sampler,
sourceSize,
destinationSize,
destinationSize,
ratio,
scale,
radius);
result.Initialize();
return result;
}
protected virtual void Initialize()
{
for (int i = 0; i < this.DestinationLength; i++)
{
ResizeKernel kernel = this.BuildKernel(i, i);
this.kernels[i] = kernel;
}
}
/// <summary>
/// Builds a <see cref="ResizeKernel"/> for the row <paramref name="destRowIndex"/> (in <see cref="kernels"/>)
/// referencing the data at row <paramref name="dataRowIndex"/> within <see cref="data"/>,
/// so the data reusable by other data rows.
/// </summary>
private ResizeKernel BuildKernel(int destRowIndex, int dataRowIndex)
{
double center = ((destRowIndex + .5) * this.ratio) - .5;
// Keep inside bounds.
int left = (int)TolerantMath.Ceiling(center - this.radius);
if (left < 0)
{
left = 0;
}
int right = (int)TolerantMath.Floor(center + this.radius);
if (right > this.sourceLength - 1)
{
right = this.sourceLength - 1;
}
ResizeKernel kernel = this.CreateKernel(dataRowIndex, left, right);
Span<double> kernelValues = this.tempValues.AsSpan().Slice(0, kernel.Length);
double sum = 0;
for (int j = left; j <= right; j++)
{
double value = this.sampler.GetValue((float)((j - center) / this.scale));
sum += value;
kernelValues[j - left] = value;
}
// Normalize, best to do it here rather than in the pixel loop later on.
if (sum > 0)
{
for (int j = 0; j < kernel.Length; j++)
{
// weights[w] = weights[w] / sum:
ref double kRef = ref kernelValues[j];
kRef /= sum;
}
}
kernel.Fill(kernelValues);
return kernel;
}
/// <summary>
/// Returns a <see cref="ResizeKernel"/> referencing values of <see cref="data"/>
/// at row <paramref name="dataRowIndex"/>.
/// </summary>
private unsafe ResizeKernel CreateKernel(int dataRowIndex, int left, int right)
{
int length = right - left + 1;
if (length > this.data.Width)
{
throw new InvalidOperationException(
$"Error in KernelMap.CreateKernel({dataRowIndex},{left},{right}): left > this.data.Width");
}
Span<float> rowSpan = this.data.GetRowSpan(dataRowIndex);
ref float rowReference = ref MemoryMarshal.GetReference(rowSpan);
float* rowPtr = (float*)Unsafe.AsPointer(ref rowReference);
return new ResizeKernel(left, rowPtr, length);
}
}
}

22
src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs → src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs

@ -27,8 +27,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
where TPixel : struct, IPixel<TPixel>
{
// The following fields are not immutable but are optionally created on demand.
private KernelMap horizontalKernelMap;
private KernelMap verticalKernelMap;
private ResizeKernelMap horizontalKernelMap;
private ResizeKernelMap verticalKernelMap;
/// <summary>
/// Initializes a new instance of the <see cref="ResizeProcessor{TPixel}"/> class.
@ -165,13 +165,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
// Since all image frame dimensions have to be the same we can calculate this for all frames.
MemoryAllocator memoryAllocator = source.GetMemoryAllocator();
this.horizontalKernelMap = KernelMap.Calculate(
this.horizontalKernelMap = ResizeKernelMap.Calculate(
this.Sampler,
this.ResizeRectangle.Width,
sourceRectangle.Width,
memoryAllocator);
this.verticalKernelMap = KernelMap.Calculate(
this.verticalKernelMap = ResizeKernelMap.Calculate(
this.Sampler,
this.ResizeRectangle.Height,
sourceRectangle.Height,
@ -254,8 +254,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRow = source.GetPixelRowSpan(y);
Span<Vector4> tempRowSpan = tempRowBuffer.Span;
Span<TPixel> sourceRow = source.GetPixelRowSpan(y).Slice(sourceX);
Span<Vector4> tempRowSpan = tempRowBuffer.Span.Slice(sourceX);
PixelOperations<TPixel>.Instance.ToVector4(configuration, sourceRow, tempRowSpan);
Vector4Utils.Premultiply(tempRowSpan);
@ -269,9 +269,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
for (int x = minX; x < maxX; x++)
{
ResizeKernel window = this.horizontalKernelMap.Kernels[x - startX];
ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - startX);
Unsafe.Add(ref firstPassBaseRef, x * sourceHeight) =
window.Convolve(tempRowSpan, sourceX);
kernel.Convolve(tempRowSpan);
}
}
});
@ -289,16 +289,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
for (int y = rows.Min; y < rows.Max; y++)
{
// Ensure offsets are normalized for cropping and padding.
ResizeKernel window = this.verticalKernelMap.Kernels[y - startY];
ResizeKernel kernel = this.verticalKernelMap.GetKernel(y - startY);
ref Vector4 tempRowBase = ref MemoryMarshal.GetReference(tempRowSpan);
for (int x = 0; x < width; x++)
{
Span<Vector4> firstPassColumn = firstPassPixelsTransposed.GetRowSpan(x);
Span<Vector4> firstPassColumn = firstPassPixelsTransposed.GetRowSpan(x).Slice(sourceY);
// Destination color components
Unsafe.Add(ref tempRowBase, x) = window.Convolve(firstPassColumn, sourceY);
Unsafe.Add(ref tempRowBase, x) = kernel.Convolve(firstPassColumn);
}
Vector4Utils.UnPremultiply(tempRowSpan);

20
src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs

@ -2,12 +2,12 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Threading.Tasks;
using System.Numerics;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.MetaData.Profiles.Exif;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// Provides methods that allow the rotating of images.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class RotateProcessor<TPixel> : CenteredAffineTransformProcessor<TPixel>
internal class RotateProcessor<TPixel> : AffineTransformProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
@ -36,9 +36,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="sampler">The sampler to perform the rotating operation.</param>
/// <param name="sourceSize">The source image size</param>
public RotateProcessor(float degrees, IResampler sampler, Size sourceSize)
: base(Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty), sampler, sourceSize)
: this(
TransformUtils.CreateRotationMatrixDegrees(degrees, sourceSize),
sampler,
sourceSize)
=> this.Degrees = degrees;
// Helper constructor
private RotateProcessor(Matrix3x2 rotationMatrix, IResampler sampler, Size sourceSize)
: base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(sourceSize, rotationMatrix))
{
this.Degrees = degrees;
}
/// <summary>
@ -84,7 +91,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <returns>The <see cref="float"/></returns>
private static float WrapDegrees(float degrees)
{
degrees = degrees % 360;
degrees %= 360;
while (degrees < 0)
{
@ -223,7 +230,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int newX = height - y - 1;
for (int x = 0; x < width; x++)
{
// TODO: Optimize this:
if (destinationBounds.Contains(newX, x))
{
destination[newX, x] = sourceRow[x];

16
src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs

@ -1,8 +1,9 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
@ -11,7 +12,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// Provides methods that allow the skewing of images.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class SkewProcessor<TPixel> : CenteredAffineTransformProcessor<TPixel>
internal class SkewProcessor<TPixel> : AffineTransformProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
@ -33,12 +34,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="sampler">The sampler to perform the skew operation.</param>
/// <param name="sourceSize">The source image size</param>
public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize)
: base(Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty), sampler, sourceSize)
: this(
TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, sourceSize),
sampler,
sourceSize)
{
this.DegreesX = degreesX;
this.DegreesY = degreesY;
}
// Helper constructor:
private SkewProcessor(Matrix3x2 skewMatrix, IResampler sampler, Size sourceSize)
: base(skewMatrix, sampler, TransformUtils.GetTransformedSize(sourceSize, skewMatrix))
{
}
/// <summary>
/// Gets the angle of rotation along the x-axis in degrees.
/// </summary>

158
src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs

@ -1,158 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using SixLabors.ImageSharp.MetaData.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Contains helper methods for working with affine and non-affine transforms
/// </summary>
internal static class TransformHelpers
{
/// <summary>
/// Updates the dimensional metadata of a transformed image
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The image to update</param>
public static void UpdateDimensionalMetData<TPixel>(Image<TPixel> image)
where TPixel : struct, IPixel<TPixel>
{
ExifProfile profile = image.MetaData.ExifProfile;
if (profile is null)
{
return;
}
// Removing the previously stored value allows us to set a value with our own data tag if required.
if (profile.GetValue(ExifTag.PixelXDimension) != null)
{
profile.RemoveValue(ExifTag.PixelXDimension);
if (image.Width <= ushort.MaxValue)
{
profile.SetValue(ExifTag.PixelXDimension, (ushort)image.Width);
}
else
{
profile.SetValue(ExifTag.PixelXDimension, (uint)image.Width);
}
}
if (profile.GetValue(ExifTag.PixelYDimension) != null)
{
profile.RemoveValue(ExifTag.PixelYDimension);
if (image.Height <= ushort.MaxValue)
{
profile.SetValue(ExifTag.PixelYDimension, (ushort)image.Height);
}
else
{
profile.SetValue(ExifTag.PixelYDimension, (uint)image.Height);
}
}
}
/// <summary>
/// Gets the centered transform matrix based upon the source and destination rectangles
/// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="destinationRectangle">The destination image bounds.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>The <see cref="Matrix3x2"/></returns>
public static Matrix3x2 GetCenteredTransformMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle, Matrix3x2 matrix)
{
// 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);
var translationToTargetCenter = Matrix3x2.CreateTranslation(-destinationRectangle.Width * .5F, -destinationRectangle.Height * .5F);
var translateToSourceCenter = Matrix3x2.CreateTranslation(sourceRectangle.Width * .5F, sourceRectangle.Height * .5F);
Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered);
// Translate back to world to pass to the Transform method.
return centered;
}
/// <summary>
/// Gets the centered transform matrix based upon the source and destination rectangles
/// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="destinationRectangle">The destination image bounds.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>The <see cref="Matrix4x4"/></returns>
public static Matrix4x4 GetCenteredTransformMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle, Matrix4x4 matrix)
{
// We invert the matrix to handle the transformation from screen to world space.
// This ensures scaling matrices are correct.
Matrix4x4.Invert(matrix, out Matrix4x4 inverted);
var translationToTargetCenter = Matrix4x4.CreateTranslation(-destinationRectangle.Width * .5F, -destinationRectangle.Height * .5F, 0);
var translateToSourceCenter = Matrix4x4.CreateTranslation(sourceRectangle.Width * .5F, sourceRectangle.Height * .5F, 0);
Matrix4x4.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix4x4 centered);
// Translate back to world to pass to the Transform method.
return centered;
}
/// <summary>
/// Returns the bounding rectangle relative to the source for the given transformation matrix.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>
/// The <see cref="Rectangle"/>.
/// </returns>
public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix)
{
// Calculate the position of the four corners in world space by applying
// The world matrix to the four corners in object space (0, 0, width, height)
var tl = Vector2.Transform(Vector2.Zero, matrix);
var tr = Vector2.Transform(new Vector2(rectangle.Width, 0), matrix);
var bl = Vector2.Transform(new Vector2(0, rectangle.Height), matrix);
var br = Vector2.Transform(new Vector2(rectangle.Width, rectangle.Height), matrix);
return GetBoundingRectangle(tl, tr, bl, br);
}
/// <summary>
/// Returns the bounding rectangle relative to the source for the given transformation matrix.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>
/// The <see cref="Rectangle"/>.
/// </returns>
public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix4x4 matrix)
{
// Calculate the position of the four corners in world space by applying
// The world matrix to the four corners in object space (0, 0, width, height)
var tl = Vector2.Transform(Vector2.Zero, matrix);
var tr = Vector2.Transform(new Vector2(rectangle.Width, 0), matrix);
var bl = Vector2.Transform(new Vector2(0, rectangle.Height), matrix);
var br = Vector2.Transform(new Vector2(rectangle.Width, rectangle.Height), matrix);
return GetBoundingRectangle(tl, tr, bl, br);
}
private static Rectangle GetBoundingRectangle(Vector2 tl, Vector2 tr, Vector2 bl, Vector2 br)
{
// Find the minimum and maximum "corners" based on the given vectors
float minX = MathF.Min(tl.X, MathF.Min(tr.X, MathF.Min(bl.X, br.X)));
float maxX = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X)));
float minY = MathF.Min(tl.Y, MathF.Min(tr.Y, MathF.Min(bl.Y, br.Y)));
float maxY = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y)));
float sizeX = maxX - minX + .5F;
float sizeY = maxY - minY + .5F;
return new Rectangle((int)(MathF.Ceiling(minX) - .5F), (int)(MathF.Ceiling(minY) - .5F), (int)MathF.Floor(sizeX), (int)MathF.Floor(sizeY));
}
}
}

161
src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs

@ -0,0 +1,161 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Contains the methods required to calculate transform kernel convolution.
/// </summary>
internal class TransformKernelMap : IDisposable
{
private readonly Buffer2D<float> yBuffer;
private readonly Buffer2D<float> xBuffer;
private readonly Vector2 extents;
private Vector4 maxSourceExtents;
private readonly IResampler sampler;
/// <summary>
/// Initializes a new instance of the <see cref="TransformKernelMap"/> class.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="source">The source size.</param>
/// <param name="destination">The destination size.</param>
/// <param name="sampler">The sampler.</param>
public TransformKernelMap(Configuration configuration, Size source, Size destination, IResampler sampler)
{
this.sampler = sampler;
float yRadius = this.GetSamplingRadius(source.Height, destination.Height);
float xRadius = this.GetSamplingRadius(source.Width, destination.Width);
this.extents = new Vector2(xRadius, yRadius);
int xLength = (int)MathF.Ceiling((this.extents.X * 2) + 2);
int yLength = (int)MathF.Ceiling((this.extents.Y * 2) + 2);
// We use 2D buffers so that we can access the weight spans per row in parallel.
this.yBuffer = configuration.MemoryAllocator.Allocate2D<float>(yLength, destination.Height);
this.xBuffer = configuration.MemoryAllocator.Allocate2D<float>(xLength, destination.Height);
int maxX = source.Width - 1;
int maxY = source.Height - 1;
this.maxSourceExtents = new Vector4(maxX, maxY, maxX, maxY);
}
/// <summary>
/// Gets a reference to the first item of the y window.
/// </summary>
/// <returns>The reference to the first item of the window.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public ref float GetYStartReference(int y)
=> ref MemoryMarshal.GetReference(this.yBuffer.GetRowSpan(y));
/// <summary>
/// Gets a reference to the first item of the x window.
/// </summary>
/// <returns>The reference to the first item of the window.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public ref float GetXStartReference(int y)
=> ref MemoryMarshal.GetReference(this.xBuffer.GetRowSpan(y));
public void Convolve<TPixel>(
Vector2 transformedPoint,
int column,
ref float ySpanRef,
ref float xSpanRef,
Buffer2D<TPixel> sourcePixels,
Span<Vector4> targetRow)
where TPixel : struct, IPixel<TPixel>
{
// Clamp sampling pixel radial extents to the source image edges
Vector2 minXY = transformedPoint - this.extents;
Vector2 maxXY = transformedPoint + this.extents;
// left, top, right, bottom
var extents = new Vector4(
MathF.Ceiling(minXY.X - .5F),
MathF.Ceiling(minXY.Y - .5F),
MathF.Floor(maxXY.X + .5F),
MathF.Floor(maxXY.Y + .5F));
extents = Vector4.Clamp(extents, Vector4.Zero, this.maxSourceExtents);
int left = (int)extents.X;
int top = (int)extents.Y;
int right = (int)extents.Z;
int bottom = (int)extents.W;
if (left == right || top == bottom)
{
return;
}
this.CalculateWeights(top, bottom, transformedPoint.Y, ref ySpanRef);
this.CalculateWeights(left, right, transformedPoint.X, ref xSpanRef);
Vector4 sum = Vector4.Zero;
for (int kernelY = 0, y = top; y <= bottom; y++, kernelY++)
{
float yWeight = Unsafe.Add(ref ySpanRef, kernelY);
for (int kernelX = 0, x = left; x <= right; x++, kernelX++)
{
float xWeight = Unsafe.Add(ref xSpanRef, kernelX);
// Values are first premultiplied to prevent darkening of edge pixels.
var current = sourcePixels[x, y].ToVector4();
Vector4Utils.Premultiply(ref current);
sum += current * xWeight * yWeight;
}
}
// Reverse the premultiplication
Vector4Utils.UnPremultiply(ref sum);
targetRow[column] = sum;
}
/// <summary>
/// Calculated the normalized weights for the given point.
/// </summary>
/// <param name="min">The minimum sampling offset</param>
/// <param name="max">The maximum sampling offset</param>
/// <param name="point">The transformed point dimension</param>
/// <param name="weightsRef">The reference to the collection of weights</param>
[MethodImpl(InliningOptions.ShortMethod)]
private void CalculateWeights(int min, int max, float point, ref float weightsRef)
{
float sum = 0;
for (int x = 0, i = min; i <= max; i++, x++)
{
float weight = this.sampler.GetValue(i - point);
sum += weight;
Unsafe.Add(ref weightsRef, x) = weight;
}
}
[MethodImpl(InliningOptions.ShortMethod)]
private float GetSamplingRadius(int sourceSize, int destinationSize)
{
float scale = (float)sourceSize / destinationSize;
if (scale < 1F)
{
scale = 1F;
}
return MathF.Ceiling(scale * this.sampler.Radius);
}
public void Dispose()
{
this.yBuffer?.Dispose();
this.xBuffer?.Dispose();
}
}
}

2
src/ImageSharp/Processing/Processors/Transforms/TransformProcessorBase.cs

@ -16,6 +16,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <inheritdoc/>
protected override void AfterImageApply(Image<TPixel> source, Image<TPixel> destination, Rectangle sourceRectangle)
=> TransformHelpers.UpdateDimensionalMetData(destination);
=> TransformProcessorHelpers.UpdateDimensionalMetData(destination);
}
}

58
src/ImageSharp/Processing/Processors/Transforms/TransformProcessorHelpers.cs

@ -0,0 +1,58 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.MetaData.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Contains helper methods for working with transforms.
/// </summary>
internal static class TransformProcessorHelpers
{
/// <summary>
/// Updates the dimensional metadata of a transformed image
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The image to update</param>
public static void UpdateDimensionalMetData<TPixel>(Image<TPixel> image)
where TPixel : struct, IPixel<TPixel>
{
ExifProfile profile = image.MetaData.ExifProfile;
if (profile is null)
{
return;
}
// Removing the previously stored value allows us to set a value with our own data tag if required.
if (profile.GetValue(ExifTag.PixelXDimension) != null)
{
profile.RemoveValue(ExifTag.PixelXDimension);
if (image.Width <= ushort.MaxValue)
{
profile.SetValue(ExifTag.PixelXDimension, (ushort)image.Width);
}
else
{
profile.SetValue(ExifTag.PixelXDimension, (uint)image.Width);
}
}
if (profile.GetValue(ExifTag.PixelYDimension) != null)
{
profile.RemoveValue(ExifTag.PixelYDimension);
if (image.Height <= ushort.MaxValue)
{
profile.SetValue(ExifTag.PixelYDimension, (ushort)image.Height);
}
else
{
profile.SetValue(ExifTag.PixelYDimension, (uint)image.Height);
}
}
}
}
}

332
src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs

@ -0,0 +1,332 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Contains utility methods for working with transforms.
/// </summary>
internal static class TransformUtils
{
/// <summary>
/// Creates a centered rotation matrix using the given rotation in degrees and the source size.
/// </summary>
/// <param name="degrees">The amount of rotation, in degrees.</param>
/// <param name="size">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
public static Matrix3x2 CreateRotationMatrixDegrees(float degrees, Size size)
=> CreateCenteredTransformMatrix(
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty));
/// <summary>
/// Creates a centered rotation matrix using the given rotation in radians and the source size.
/// </summary>
/// <param name="radians">The amount of rotation, in radians.</param>
/// <param name="size">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
public static Matrix3x2 CreateRotationMatrixRadians(float radians, Size size)
=> CreateCenteredTransformMatrix(
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateRotation(radians, PointF.Empty));
/// <summary>
/// Creates a centered skew matrix from the give angles in degrees and the 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>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
public static Matrix3x2 CreateSkewMatrixDegrees(float degreesX, float degreesY, Size size)
=> CreateCenteredTransformMatrix(
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty));
/// <summary>
/// Creates a centered skew matrix from the give angles in radians and the 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>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
public static Matrix3x2 CreateSkewMatrixRadians(float radiansX, float radiansY, Size size)
=> CreateCenteredTransformMatrix(
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty));
/// <summary>
/// Gets the centered transform matrix based upon the source and destination rectangles.
/// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>The <see cref="Matrix3x2"/></returns>
public static Matrix3x2 CreateCenteredTransformMatrix(Rectangle sourceRectangle, Matrix3x2 matrix)
{
Rectangle destinationRectangle = GetTransformedBoundingRectangle(sourceRectangle, matrix);
// 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);
var translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-destinationRectangle.Width, -destinationRectangle.Height) * .5F);
var translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width, sourceRectangle.Height) * .5F);
// Translate back to world space.
Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered);
return centered;
}
/// <summary>
/// Creates a matrix that performs a tapering projective transform.
/// <see href="https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/transforms/non-affine"/>
/// </summary>
/// <param name="size">The rectangular size of the image being transformed.</param>
/// <param name="side">An enumeration that indicates the side of the rectangle that tapers.</param>
/// <param name="corner">An enumeration that indicates on which corners to taper the rectangle.</param>
/// <param name="fraction">The amount to taper.</param>
/// <returns>The <see cref="Matrix4x4"/></returns>
public static Matrix4x4 CreateTaperMatrix(Size size, TaperSide side, TaperCorner corner, float fraction)
{
Matrix4x4 matrix = Matrix4x4.Identity;
switch (side)
{
case TaperSide.Left:
matrix.M11 = fraction;
matrix.M22 = fraction;
matrix.M13 = (fraction - 1) / size.Width;
switch (corner)
{
case TaperCorner.RightOrBottom:
break;
case TaperCorner.LeftOrTop:
matrix.M12 = size.Height * matrix.M13;
matrix.M32 = size.Height * (1 - fraction);
break;
case TaperCorner.Both:
matrix.M12 = size.Height * .5F * matrix.M13;
matrix.M32 = size.Height * (1 - fraction) / 2;
break;
}
break;
case TaperSide.Top:
matrix.M11 = fraction;
matrix.M22 = fraction;
matrix.M23 = (fraction - 1) / size.Height;
switch (corner)
{
case TaperCorner.RightOrBottom:
break;
case TaperCorner.LeftOrTop:
matrix.M21 = size.Width * matrix.M23;
matrix.M31 = size.Width * (1 - fraction);
break;
case TaperCorner.Both:
matrix.M21 = size.Width * .5F * matrix.M23;
matrix.M31 = size.Width * (1 - fraction) / 2;
break;
}
break;
case TaperSide.Right:
matrix.M11 = 1 / fraction;
matrix.M13 = (1 - fraction) / (size.Width * fraction);
switch (corner)
{
case TaperCorner.RightOrBottom:
break;
case TaperCorner.LeftOrTop:
matrix.M12 = size.Height * matrix.M13;
break;
case TaperCorner.Both:
matrix.M12 = size.Height * .5F * matrix.M13;
break;
}
break;
case TaperSide.Bottom:
matrix.M22 = 1 / fraction;
matrix.M23 = (1 - fraction) / (size.Height * fraction);
switch (corner)
{
case TaperCorner.RightOrBottom:
break;
case TaperCorner.LeftOrTop:
matrix.M21 = size.Width * matrix.M23;
break;
case TaperCorner.Both:
matrix.M21 = size.Width * .5F * matrix.M23;
break;
}
break;
}
return matrix;
}
/// <summary>
/// Returns the rectangle bounds relative to the source for the given transformation matrix.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>
/// The <see cref="Rectangle"/>.
/// </returns>
public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix)
{
Rectangle transformed = GetTransformedRectangle(rectangle, matrix);
return new Rectangle(0, 0, transformed.Width, transformed.Height);
}
/// <summary>
/// Returns the rectangle relative to the source for the given transformation matrix.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>
/// The <see cref="Rectangle"/>.
/// </returns>
public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix3x2 matrix)
{
if (rectangle.Equals(default) || Matrix3x2.Identity.Equals(matrix))
{
return rectangle;
}
var tl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix);
var tr = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix);
var bl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix);
var br = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix);
return GetBoundingRectangle(tl, tr, bl, br);
}
/// <summary>
/// Returns the size relative to the source for the given transformation matrix.
/// </summary>
/// <param name="size">The source size.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>
/// The <see cref="Size"/>.
/// </returns>
public static Size GetTransformedSize(Size size, Matrix3x2 matrix)
{
Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!");
if (matrix.Equals(default) || matrix.Equals(Matrix3x2.Identity))
{
return size;
}
Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix);
return ConstrainSize(rectangle);
}
/// <summary>
/// Returns the rectangle relative to the source for the given transformation matrix.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>
/// The <see cref="Rectangle"/>.
/// </returns>
public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix4x4 matrix)
{
if (rectangle.Equals(default) || Matrix4x4.Identity.Equals(matrix))
{
return rectangle;
}
Vector2 GetVector(float x, float y)
{
const float Epsilon = 0.0000001F;
var v3 = Vector3.Transform(new Vector3(x, y, 1F), matrix);
return new Vector2(v3.X, v3.Y) / MathF.Max(v3.Z, Epsilon);
}
Vector2 tl = GetVector(rectangle.Left, rectangle.Top);
Vector2 tr = GetVector(rectangle.Right, rectangle.Top);
Vector2 bl = GetVector(rectangle.Left, rectangle.Bottom);
Vector2 br = GetVector(rectangle.Right, rectangle.Bottom);
return GetBoundingRectangle(tl, tr, bl, br);
}
/// <summary>
/// Returns the size relative to the source for the given transformation matrix.
/// </summary>
/// <param name="size">The source size.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>
/// The <see cref="Size"/>.
/// </returns>
public static Size GetTransformedSize(Size size, Matrix4x4 matrix)
{
Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!");
if (matrix.Equals(default) || matrix.Equals(Matrix4x4.Identity))
{
return size;
}
Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix);
return ConstrainSize(rectangle);
}
private static Size ConstrainSize(Rectangle 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 eith direction we need to reassign the dimension.
if (height <= 0)
{
height = rectangle.Height;
}
if (width <= 0)
{
width = rectangle.Width;
}
return new Size(width, height);
}
private static Rectangle 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)));
return Rectangle.Round(RectangleF.FromLTRB(left, top, right, bottom));
}
}
}

319
src/ImageSharp/Processing/ProjectiveTransformBuilder.cs

@ -0,0 +1,319 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.Numerics;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing
{
/// <summary>
/// A helper class for constructing <see cref="Matrix4x4"/> instances for use in projective transforms.
/// </summary>
public class ProjectiveTransformBuilder
{
private readonly List<Func<Size, Matrix4x4>> matrixFactories = new List<Func<Size, Matrix4x4>>();
/// <summary>
/// Prepends a matrix that performs a tapering projective transform.
/// </summary>
/// <param name="side">An enumeration that indicates the side of the rectangle that tapers.</param>
/// <param name="corner">An enumeration that indicates on which corners to taper the rectangle.</param>
/// <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));
/// <summary>
/// Appends a matrix that performs a tapering projective transform.
/// </summary>
/// <param name="side">An enumeration that indicates the side of the rectangle that tapers.</param>
/// <param name="corner">An enumeration that indicates on which corners to taper the rectangle.</param>
/// <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));
/// <summary>
/// Prepends a centered rotation matrix using the given rotation in degrees.
/// </summary>
/// <param name="degrees">The amount of rotation, in degrees.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependRotationDegrees(float degrees)
=> this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees));
/// <summary>
/// Prepends a centered rotation matrix using the given rotation in radians.
/// </summary>
/// <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.CreateRotationMatrixRadians(radians, size)));
/// <summary>
/// Prepends a centered rotation matrix using the given rotation in degrees at the given origin.
/// </summary>
/// <param name="degrees">The amount of rotation, in radians.</param>
/// <param name="origin">The rotation origin point.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
internal ProjectiveTransformBuilder PrependRotationDegrees(float degrees, Vector2 origin)
=> this.PrependRotationRadians(GeometryUtilities.DegreeToRadian(degrees), origin);
/// <summary>
/// Prepends a centered rotation matrix using the given rotation in radians at the given origin.
/// </summary>
/// <param name="radians">The amount of rotation, in radians.</param>
/// <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)));
/// <summary>
/// Appends a centered rotation matrix using the given rotation in degrees.
/// </summary>
/// <param name="degrees">The amount of rotation, in degrees.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendRotationDegrees(float degrees)
=> this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees));
/// <summary>
/// Appends a centered rotation matrix using the given rotation in radians.
/// </summary>
/// <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.CreateRotationMatrixRadians(radians, size)));
/// <summary>
/// Appends a centered rotation matrix using the given rotation in degrees at the given origin.
/// </summary>
/// <param name="degrees">The amount of rotation, in radians.</param>
/// <param name="origin">The rotation origin point.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
internal ProjectiveTransformBuilder AppendRotationDegrees(float degrees, Vector2 origin)
=> this.AppendRotationRadians(GeometryUtilities.DegreeToRadian(degrees), origin);
/// <summary>
/// Appends a centered rotation matrix using the given rotation in radians at the given origin.
/// </summary>
/// <param name="radians">The amount of rotation, in radians.</param>
/// <param name="origin">The rotation origin point.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
internal ProjectiveTransformBuilder AppendRotationRadians(float radians, Vector2 origin)
=> this.AppendMatrix(Matrix4x4.CreateRotationZ(radians, new Vector3(origin, 0)));
/// <summary>
/// Prepends a scale matrix from the given uniform scale.
/// </summary>
/// <param name="scale">The uniform scale.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependScale(float scale)
=> this.PrependMatrix(Matrix4x4.CreateScale(scale));
/// <summary>
/// Prepends a scale matrix from the given vector scale.
/// </summary>
/// <param name="scale">The horizontal and vertical scale.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependScale(SizeF scale)
=> this.PrependScale((Vector2)scale);
/// <summary>
/// Prepends a scale matrix from the given vector scale.
/// </summary>
/// <param name="scales">The horizontal and vertical scale.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependScale(Vector2 scales)
=> this.PrependMatrix(Matrix4x4.CreateScale(new Vector3(scales, 1F)));
/// <summary>
/// Appends a scale matrix from the given uniform scale.
/// </summary>
/// <param name="scale">The uniform scale.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendScale(float scale)
=> this.AppendMatrix(Matrix4x4.CreateScale(scale));
/// <summary>
/// Appends a scale matrix from the given vector scale.
/// </summary>
/// <param name="scales">The horizontal and vertical scale.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendScale(SizeF scales)
=> this.AppendScale((Vector2)scales);
/// <summary>
/// Appends a scale matrix from the given vector scale.
/// </summary>
/// <param name="scales">The horizontal and vertical scale.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendScale(Vector2 scales)
=> this.AppendMatrix(Matrix4x4.CreateScale(new Vector3(scales, 1F)));
/// <summary>
/// Prepends a centered skew matrix from the give angles in degrees.
/// </summary>
/// <param name="degreesX">The X angle, in degrees.</param>
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
internal ProjectiveTransformBuilder PrependSkewDegrees(float degreesX, float degreesY)
=> this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY));
/// <summary>
/// Prepends a centered skew matrix from the give angles in radians.
/// </summary>
/// <param name="radiansX">The X angle, in radians.</param>
/// <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.CreateSkewMatrixRadians(radiansX, radiansY, size)));
/// <summary>
/// Prepends a skew matrix using the given angles in degrees at the given origin.
/// </summary>
/// <param name="degreesX">The X angle, in degrees.</param>
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <param name="origin">The skew origin point.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependSkewDegrees(float degreesX, float degreesY, Vector2 origin)
=> this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), origin);
/// <summary>
/// Prepends a skew matrix using the given angles in radians at the given origin.
/// </summary>
/// <param name="radiansX">The X angle, in radians.</param>
/// <param name="radiansY">The Y angle, in radians.</param>
/// <param name="origin">The skew origin point.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY, Vector2 origin)
=> this.PrependMatrix(new Matrix4x4(Matrix3x2.CreateSkew(radiansX, radiansY, origin)));
/// <summary>
/// Appends a centered skew matrix from the give angles in degrees.
/// </summary>
/// <param name="degreesX">The X angle, in degrees.</param>
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
internal ProjectiveTransformBuilder AppendSkewDegrees(float degreesX, float degreesY)
=> this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY));
/// <summary>
/// Appends a centered skew matrix from the give angles in radians.
/// </summary>
/// <param name="radiansX">The X angle, in radians.</param>
/// <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.CreateSkewMatrixRadians(radiansX, radiansY, size)));
/// <summary>
/// Appends a skew matrix using the given angles in degrees at the given origin.
/// </summary>
/// <param name="degreesX">The X angle, in degrees.</param>
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <param name="origin">The skew origin point.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendSkewDegrees(float degreesX, float degreesY, Vector2 origin)
=> this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), origin);
/// <summary>
/// Appends a skew matrix using the given angles in radians at the given origin.
/// </summary>
/// <param name="radiansX">The X angle, in radians.</param>
/// <param name="radiansY">The Y angle, in radians.</param>
/// <param name="origin">The skew origin point.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY, Vector2 origin)
=> this.AppendMatrix(new Matrix4x4(Matrix3x2.CreateSkew(radiansX, radiansY, origin)));
/// <summary>
/// Prepends a translation matrix from the given vector.
/// </summary>
/// <param name="position">The translation position.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependTranslation(PointF position)
=> this.PrependTranslation((Vector2)position);
/// <summary>
/// Prepends a translation matrix from the given vector.
/// </summary>
/// <param name="position">The translation position.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependTranslation(Vector2 position)
=> this.PrependMatrix(Matrix4x4.CreateTranslation(new Vector3(position, 0)));
/// <summary>
/// Appends a translation matrix from the given vector.
/// </summary>
/// <param name="position">The translation position.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendTranslation(PointF position)
=> this.AppendTranslation((Vector2)position);
/// <summary>
/// Appends a translation matrix from the given vector.
/// </summary>
/// <param name="position">The translation position.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendTranslation(Vector2 position)
=> this.AppendMatrix(Matrix4x4.CreateTranslation(new Vector3(position, 0)));
/// <summary>
/// Prepends a raw matrix.
/// </summary>
/// <param name="matrix">The matrix to prepend.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependMatrix(Matrix4x4 matrix) => this.Prepend(_ => matrix);
/// <summary>
/// Appends a raw matrix.
/// </summary>
/// <param name="matrix">The matrix to append.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendMatrix(Matrix4x4 matrix) => this.Append(_ => matrix);
/// <summary>
/// Returns the combined matrix for a given source size.
/// </summary>
/// <param name="sourceSize">The source image size.</param>
/// <returns>The <see cref="Matrix4x4"/>.</returns>
public Matrix4x4 BuildMatrix(Size sourceSize) => this.BuildMatrix(new Rectangle(Point.Empty, sourceSize));
/// <summary>
/// Returns the combined matrix for a given source rectangle.
/// </summary>
/// <param name="sourceRectangle">The rectangle in the source image.</param>
/// <returns>The <see cref="Matrix4x4"/>.</returns>
public Matrix4x4 BuildMatrix(Rectangle sourceRectangle)
{
Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle));
Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle));
// Translate the origin matrix to cater for source rectangle offsets.
var matrix = Matrix4x4.CreateTranslation(new Vector3(-sourceRectangle.Location, 0));
Size size = sourceRectangle.Size;
foreach (Func<Size, Matrix4x4> factory in this.matrixFactories)
{
matrix *= factory(size);
}
return matrix;
}
private ProjectiveTransformBuilder Prepend(Func<Size, Matrix4x4> factory)
{
this.matrixFactories.Insert(0, factory);
return this;
}
private ProjectiveTransformBuilder Append(Func<Size, Matrix4x4> factory)
{
this.matrixFactories.Add(factory);
return this;
}
}
}

166
src/ImageSharp/Processing/ProjectiveTransformHelper.cs

@ -1,166 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing
{
/// <summary>
/// Enumerates the various options which determine which side to taper
/// </summary>
public enum TaperSide
{
/// <summary>
/// Taper the left side
/// </summary>
Left,
/// <summary>
/// Taper the top side
/// </summary>
Top,
/// <summary>
/// Taper the right side
/// </summary>
Right,
/// <summary>
/// Taper the bottom side
/// </summary>
Bottom
}
/// <summary>
/// Enumerates the various options which determine how to taper corners
/// </summary>
public enum TaperCorner
{
/// <summary>
/// Taper the left or top corner
/// </summary>
LeftOrTop,
/// <summary>
/// Taper the right or bottom corner
/// </summary>
RightOrBottom,
/// <summary>
/// Taper the both sets of corners
/// </summary>
Both
}
/// <summary>
/// Provides helper methods for working with generalized projective transforms.
/// </summary>
public static class ProjectiveTransformHelper
{
/// <summary>
/// Creates a matrix that performs a tapering projective transform.
/// <see href="https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/transforms/non-affine"/>
/// </summary>
/// <param name="size">The rectangular size of the image being transformed.</param>
/// <param name="taperSide">An enumeration that indicates the side of the rectangle that tapers.</param>
/// <param name="taperCorner">An enumeration that indicates on which corners to taper the rectangle.</param>
/// <param name="taperFraction">The amount to taper.</param>
/// <returns>The <see cref="Matrix4x4"/></returns>
public static Matrix4x4 CreateTaperMatrix(Size size, TaperSide taperSide, TaperCorner taperCorner, float taperFraction)
{
Matrix4x4 matrix = Matrix4x4.Identity;
switch (taperSide)
{
case TaperSide.Left:
matrix.M11 = taperFraction;
matrix.M22 = taperFraction;
matrix.M13 = (taperFraction - 1) / size.Width;
switch (taperCorner)
{
case TaperCorner.RightOrBottom:
break;
case TaperCorner.LeftOrTop:
matrix.M12 = size.Height * matrix.M13;
matrix.M32 = size.Height * (1 - taperFraction);
break;
case TaperCorner.Both:
matrix.M12 = (size.Height * 0.5f) * matrix.M13;
matrix.M32 = size.Height * (1 - taperFraction) / 2;
break;
}
break;
case TaperSide.Top:
matrix.M11 = taperFraction;
matrix.M22 = taperFraction;
matrix.M23 = (taperFraction - 1) / size.Height;
switch (taperCorner)
{
case TaperCorner.RightOrBottom:
break;
case TaperCorner.LeftOrTop:
matrix.M21 = size.Width * matrix.M23;
matrix.M31 = size.Width * (1 - taperFraction);
break;
case TaperCorner.Both:
matrix.M21 = (size.Width * 0.5f) * matrix.M23;
matrix.M31 = size.Width * (1 - taperFraction) / 2;
break;
}
break;
case TaperSide.Right:
matrix.M11 = 1 / taperFraction;
matrix.M13 = (1 - taperFraction) / (size.Width * taperFraction);
switch (taperCorner)
{
case TaperCorner.RightOrBottom:
break;
case TaperCorner.LeftOrTop:
matrix.M12 = size.Height * matrix.M13;
break;
case TaperCorner.Both:
matrix.M12 = (size.Height * 0.5f) * matrix.M13;
break;
}
break;
case TaperSide.Bottom:
matrix.M22 = 1 / taperFraction;
matrix.M23 = (1 - taperFraction) / (size.Height * taperFraction);
switch (taperCorner)
{
case TaperCorner.RightOrBottom:
break;
case TaperCorner.LeftOrTop:
matrix.M21 = size.Width * matrix.M23;
break;
case TaperCorner.Both:
matrix.M21 = (size.Width * 0.5f) * matrix.M23;
break;
}
break;
}
return matrix;
}
}
}

26
src/ImageSharp/Processing/TaperCorner.cs

@ -0,0 +1,26 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing
{
/// <summary>
/// Enumerates the various options which determine how to taper corners
/// </summary>
public enum TaperCorner
{
/// <summary>
/// Taper the left or top corner
/// </summary>
LeftOrTop,
/// <summary>
/// Taper the right or bottom corner
/// </summary>
RightOrBottom,
/// <summary>
/// Taper the both sets of corners
/// </summary>
Both
}
}

31
src/ImageSharp/Processing/TaperSide.cs

@ -0,0 +1,31 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing
{
/// <summary>
/// Enumerates the various options which determine which side to taper
/// </summary>
public enum TaperSide
{
/// <summary>
/// Taper the left side
/// </summary>
Left,
/// <summary>
/// Taper the top side
/// </summary>
Top,
/// <summary>
/// Taper the right side
/// </summary>
Right,
/// <summary>
/// Taper the bottom side
/// </summary>
Bottom
}
}

141
src/ImageSharp/Processing/TransformExtensions.cs

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives;
@ -14,109 +15,147 @@ namespace SixLabors.ImageSharp.Processing
public static class TransformExtensions
{
/// <summary>
/// Transforms an image by the given matrix.
/// Performs an affine transform of an image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="builder">The affine transform builder.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix3x2 matrix)
public static IImageProcessingContext<TPixel> Transform<TPixel>(
this IImageProcessingContext<TPixel> source,
AffineTransformBuilder builder)
where TPixel : struct, IPixel<TPixel>
=> Transform(source, matrix, KnownResamplers.Bicubic);
=> Transform(source, builder, KnownResamplers.Bicubic);
/// <summary>
/// Transforms an image by the given matrix using the specified sampling algorithm.
/// Performs an affine transform of an image using the specified sampling algorithm.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="ctx">The <see cref="IImageProcessingContext{TPixel}"/>.</param>
/// <param name="builder">The affine transform builder.</param>
/// <param name="sampler">The <see cref="IResampler"/> to perform the resampling.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix3x2 matrix, IResampler sampler)
public static IImageProcessingContext<TPixel> Transform<TPixel>(
this IImageProcessingContext<TPixel> ctx,
AffineTransformBuilder builder,
IResampler sampler)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new AffineTransformProcessor<TPixel>(matrix, sampler, source.GetCurrentSize()));
=> ctx.Transform(new Rectangle(Point.Empty, ctx.GetCurrentSize()), builder, sampler);
/// <summary>
/// Transforms an image by the given matrix using the specified sampling algorithm
/// and a rectangle defining the transform origin in the source image and the size of the result image.
/// Performs an affine transform of an image using the specified sampling algorithm.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="ctx">The <see cref="IImageProcessingContext{TPixel}"/>.</param>
/// <param name="sourceRectangle">The source rectangle</param>
/// <param name="builder">The affine transform builder.</param>
/// <param name="sampler">The <see cref="IResampler"/> to perform the resampling.</param>
/// <param name="rectangle">
/// The rectangle defining the transform origin in the source image, and the size of the result image.
/// </param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Transform<TPixel>(
this IImageProcessingContext<TPixel> source,
Matrix3x2 matrix,
IResampler sampler,
Rectangle rectangle)
this IImageProcessingContext<TPixel> ctx,
Rectangle sourceRectangle,
AffineTransformBuilder builder,
IResampler sampler)
where TPixel : struct, IPixel<TPixel>
{
var t = Matrix3x2.CreateTranslation(-rectangle.Location);
Matrix3x2 combinedMatrix = t * matrix;
return source.ApplyProcessor(new AffineTransformProcessor<TPixel>(combinedMatrix, sampler, rectangle.Size));
Matrix3x2 transform = builder.BuildMatrix(sourceRectangle);
Size targetDimensions = TransformUtils.GetTransformedSize(sourceRectangle.Size, transform);
return ctx.Transform(sourceRectangle, transform, targetDimensions, sampler);
}
/// <summary>
/// Transforms an image by the given matrix using the specified sampling algorithm,
/// cropping or extending the image according to <paramref name="destinationSize"/>.
/// Performs an affine transform of an image using the specified sampling algorithm.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="ctx">The <see cref="IImageProcessingContext{TPixel}"/>.</param>
/// <param name="sourceRectangle">The source rectangle</param>
/// <param name="transform">The transformation matrix.</param>
/// <param name="targetDimensions">The size of the result image.</param>
/// <param name="sampler">The <see cref="IResampler"/> to perform the resampling.</param>
/// <param name="destinationSize">The size of the destination image.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Transform<TPixel>(
this IImageProcessingContext<TPixel> source,
Matrix3x2 matrix,
IResampler sampler,
Size destinationSize)
this IImageProcessingContext<TPixel> ctx,
Rectangle sourceRectangle,
Matrix3x2 transform,
Size targetDimensions,
IResampler sampler)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new AffineTransformProcessor<TPixel>(matrix, sampler, destinationSize));
{
return ctx.ApplyProcessor(
new AffineTransformProcessor<TPixel>(transform, sampler, targetDimensions),
sourceRectangle);
}
/// <summary>
/// Transforms an image by the given matrix.
/// Performs a projective transform of an image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="builder">The affine transform builder.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix4x4 matrix)
public static IImageProcessingContext<TPixel> Transform<TPixel>(
this IImageProcessingContext<TPixel> source,
ProjectiveTransformBuilder builder)
where TPixel : struct, IPixel<TPixel>
=> Transform(source, matrix, KnownResamplers.Bicubic);
=> Transform(source, builder, KnownResamplers.Bicubic);
/// <summary>
/// Applies a projective transform to the image by the given matrix using the specified sampling algorithm.
/// Performs a projective transform of an image using the specified sampling algorithm.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="ctx">The <see cref="IImageProcessingContext{TPixel}"/>.</param>
/// <param name="builder">The projective transform builder.</param>
/// <param name="sampler">The <see cref="IResampler"/> to perform the resampling.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix4x4 matrix, IResampler sampler)
public static IImageProcessingContext<TPixel> Transform<TPixel>(
this IImageProcessingContext<TPixel> ctx,
ProjectiveTransformBuilder builder,
IResampler sampler)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new ProjectiveTransformProcessor<TPixel>(matrix, sampler, source.GetCurrentSize()));
=> ctx.Transform(new Rectangle(Point.Empty, ctx.GetCurrentSize()), builder, sampler);
/// <summary>
/// Applies a projective transform to the image by the given matrix using the specified sampling algorithm.
/// TODO: Should we be offsetting the matrix here?
/// Performs a projective transform of an image using the specified sampling algorithm.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="ctx">The <see cref="IImageProcessingContext{TPixel}"/>.</param>
/// <param name="sourceRectangle">The source rectangle</param>
/// <param name="builder">The projective transform builder.</param>
/// <param name="sampler">The <see cref="IResampler"/> to perform the resampling.</param>
/// <param name="rectangle">The rectangle to constrain the transformed image to.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
internal static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix4x4 matrix, IResampler sampler, Rectangle rectangle)
public static IImageProcessingContext<TPixel> Transform<TPixel>(
this IImageProcessingContext<TPixel> ctx,
Rectangle sourceRectangle,
ProjectiveTransformBuilder builder,
IResampler sampler)
where TPixel : struct, IPixel<TPixel>
{
Matrix4x4 transform = builder.BuildMatrix(sourceRectangle);
Size targetDimensions = TransformUtils.GetTransformedSize(sourceRectangle.Size, transform);
return ctx.Transform(sourceRectangle, transform, targetDimensions, sampler);
}
/// <summary>
/// Performs a projective transform of an image using the specified sampling algorithm.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="ctx">The <see cref="IImageProcessingContext{TPixel}"/>.</param>
/// <param name="sourceRectangle">The source rectangle</param>
/// <param name="transform">The transformation matrix.</param>
/// <param name="targetDimensions">The size of the result image.</param>
/// <param name="sampler">The <see cref="IResampler"/> to perform the resampling.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Transform<TPixel>(
this IImageProcessingContext<TPixel> ctx,
Rectangle sourceRectangle,
Matrix4x4 transform,
Size targetDimensions,
IResampler sampler)
where TPixel : struct, IPixel<TPixel>
{
var t = Matrix4x4.CreateTranslation(new Vector3(-rectangle.Location, 0));
Matrix4x4 combinedMatrix = t * matrix;
return source.ApplyProcessor(new ProjectiveTransformProcessor<TPixel>(combinedMatrix, sampler, rectangle.Size));
return ctx.ApplyProcessor(
new ProjectiveTransformProcessor<TPixel>(transform, sampler, targetDimensions),
sourceRectangle);
}
}
}

45
tests/ImageSharp.Benchmarks/Samplers/Rotate.cs

@ -0,0 +1,45 @@
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Benchmarks.Samplers
{
[Config(typeof(Config.ShortClr))]
public class Rotate
{
[Benchmark]
public Size DoRotate()
{
using (var image = new Image<Rgba32>(Configuration.Default, 400, 400, Rgba32.BlanchedAlmond))
{
image.Mutate(x => x.Rotate(37.5F));
return image.Size();
}
}
}
}
// Nov 7 2018
//BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17763
//Intel Core i7-6600U CPU 2.60GHz(Skylake), 1 CPU, 4 logical and 2 physical cores
//.NET Core SDK = 2.1.403
// [Host] : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
// Job-KKDIMW : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3190.0
// Job-IUZRFA : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
//LaunchCount=1 TargetCount=3 WarmupCount=3
// #### BEFORE ####:
// Method | Runtime | Mean | Error | StdDev | Allocated |
//--------- |-------- |---------:|----------:|----------:|----------:|
// DoRotate | Clr | 85.19 ms | 13.379 ms | 0.7560 ms | 6 KB |
// DoRotate | Core | 53.51 ms | 9.512 ms | 0.5375 ms | 4.29 KB |
// #### AFTER ####:
//Method | Runtime | Mean | Error | StdDev | Allocated |
//--------- |-------- |---------:|---------:|---------:|----------:|
// DoRotate | Clr | 77.08 ms | 23.97 ms | 1.354 ms | 6 KB |
// DoRotate | Core | 40.36 ms | 47.43 ms | 2.680 ms | 4.36 KB |

45
tests/ImageSharp.Benchmarks/Samplers/Skew.cs

@ -0,0 +1,45 @@
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Benchmarks.Samplers
{
[Config(typeof(Config.ShortClr))]
public class Skew
{
[Benchmark]
public Size DoSkew()
{
using (var image = new Image<Rgba32>(Configuration.Default, 400, 400, Rgba32.BlanchedAlmond))
{
image.Mutate(x => x.Skew(20, 10));
return image.Size();
}
}
}
}
// Nov 7 2018
//BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17763
//Intel Core i7-6600U CPU 2.60GHz(Skylake), 1 CPU, 4 logical and 2 physical cores
//.NET Core SDK = 2.1.403
// [Host] : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
// Job-KKDIMW : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3190.0
// Job-IUZRFA : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
//LaunchCount=1 TargetCount=3 WarmupCount=3
// #### BEFORE ####:
//Method | Runtime | Mean | Error | StdDev | Allocated |
//------- |-------- |---------:|---------:|----------:|----------:|
// DoSkew | Clr | 78.14 ms | 8.383 ms | 0.4736 ms | 6 KB |
// DoSkew | Core | 44.22 ms | 4.109 ms | 0.2322 ms | 4.28 KB |
// #### AFTER ####:
//Method | Runtime | Mean | Error | StdDev | Allocated |
//------- |-------- |---------:|----------:|----------:|----------:|
// DoSkew | Clr | 71.63 ms | 25.589 ms | 1.4458 ms | 6 KB |
// DoSkew | Core | 38.99 ms | 8.640 ms | 0.4882 ms | 4.36 KB |

4
tests/ImageSharp.Sandbox46/Program.cs

@ -63,8 +63,8 @@ namespace SixLabors.ImageSharp.Sandbox46
private static void RunDecodeJpegProfilingTests()
{
Console.WriteLine("RunDecodeJpegProfilingTests...");
var benchmarks = new JpegBenchmarks(new ConsoleOutput());
foreach (object[] data in JpegBenchmarks.DecodeJpegData)
var benchmarks = new JpegProfilingBenchmarks(new ConsoleOutput());
foreach (object[] data in JpegProfilingBenchmarks.DecodeJpegData)
{
string fileName = (string)data[0];
benchmarks.DecodeJpeg(fileName);

5
tests/ImageSharp.Tests/Advanced/AotCompilerTests.cs

@ -10,9 +10,6 @@ namespace SixLabors.ImageSharp.Tests.Advanced
public class AotCompilerTests
{
[Fact]
public void AotCompiler_NoExceptions()
{
AotCompilerTools.Seed<Rgba32>();
}
public void AotCompiler_NoExceptions() => AotCompilerTools.Seed<Rgba32, Rgb24, Bgr24>();
}
}

22
tests/ImageSharp.Tests/Drawing/DrawImageTest.cs

@ -2,10 +2,8 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Primitives;
using Xunit;
@ -75,21 +73,15 @@ namespace SixLabors.ImageSharp.Tests
using (Image<TPixel> image = provider.GetImage())
using (var blend = Image.Load<TPixel>(TestFile.Create(TestImages.Bmp.Car).Bytes))
{
Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(45F);
Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(.25F, .25F));
Matrix3x2 matrix = rotate * scale;
AffineTransformBuilder builder = new AffineTransformBuilder()
.AppendRotationDegrees(45F)
.AppendScale(new SizeF(.25F, .25F))
.AppendTranslation(new PointF(10, 10));
// Lets center the matrix so we can tell whether any cut-off issues we may have belong to the drawing processor
Rectangle srcBounds = blend.Bounds();
Rectangle destBounds = TransformHelpers.GetTransformedBoundingRectangle(srcBounds, matrix);
Matrix3x2 centeredMatrix = TransformHelpers.GetCenteredTransformMatrix(srcBounds, destBounds, matrix);
// We pass a new rectangle here based on the dest bounds since we've offset the matrix
blend.Mutate(x => x.Transform(
centeredMatrix,
KnownResamplers.Bicubic,
new Rectangle(0, 0, destBounds.Width, destBounds.Height)));
// Apply a background color so we can see the translation.
blend.Mutate(x => x.Transform(builder).BackgroundColor(NamedColors<TPixel>.HotPink));
// Lets center the matrix so we can tell whether any cut-off issues we may have belong to the drawing processor
var position = new Point((image.Width - blend.Width) / 2, (image.Height - blend.Height) / 2);
image.Mutate(x => x.DrawImage(blend, position, mode, .75F));
image.DebugSave(provider, new[] { "Transformed" });

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

@ -49,7 +49,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
TestImages.Jpeg.Issues.NoEoiProgressive517,
TestImages.Jpeg.Issues.BadRstProgressive518,
TestImages.Jpeg.Issues.InvalidEOI695,
TestImages.Jpeg.Issues.ExifResizeOutOfRange696
TestImages.Jpeg.Issues.ExifResizeOutOfRange696,
TestImages.Jpeg.Issues.ExifGetString750Transform
};
return !TestEnvironment.Is64BitProcess && largeImagesToSkipOn32Bit.Contains(provider.SourceFileOrDescription);

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

@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
Assert.Equal(PngChunkType.Palette, GetType("PLTE"));
Assert.Equal(PngChunkType.Data, GetType("IDAT"));
Assert.Equal(PngChunkType.End, GetType("IEND"));
Assert.Equal(PngChunkType.PaletteAlpha, GetType("tRNS"));
Assert.Equal(PngChunkType.Transparency, GetType("tRNS"));
Assert.Equal(PngChunkType.Text, GetType("tEXt"));
Assert.Equal(PngChunkType.Gamma, GetType("gAMA"));
Assert.Equal(PngChunkType.Physical, GetType("pHYs"));

2
tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs

@ -76,7 +76,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
[Theory]
[InlineData((uint)PngChunkType.Gamma)] // gAMA
[InlineData((uint)PngChunkType.PaletteAlpha)] // tRNS
[InlineData((uint)PngChunkType.Transparency)] // tRNS
[InlineData((uint)PngChunkType.Physical)] // pHYs: It's ok to test physical as we don't throw for duplicate chunks.
//[InlineData(PngChunkTypes.Text)] //TODO: Figure out how to test this
public void Decode_IncorrectCRCForNonCriticalChunk_ExceptionIsThrown(uint chunkType)

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

@ -25,6 +25,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
{ TestImages.Png.Bpp1, PngBitDepth.Bit1 }
};
public static readonly TheoryData<string, PngBitDepth, PngColorType> PngTrnsFiles =
new TheoryData<string, PngBitDepth, PngColorType>
{
{ TestImages.Png.Gray1BitTrans, PngBitDepth.Bit1, PngColorType.Grayscale },
{ TestImages.Png.Gray2BitTrans, PngBitDepth.Bit2, PngColorType.Grayscale },
{ TestImages.Png.Gray4BitTrans, PngBitDepth.Bit4, PngColorType.Grayscale },
{ TestImages.Png.Gray8BitTrans, PngBitDepth.Bit8, PngColorType.Grayscale },
{ TestImages.Png.GrayTrns16BitInterlaced, PngBitDepth.Bit16, PngColorType.Grayscale },
{ TestImages.Png.Rgb24BppTrans, PngBitDepth.Bit8, PngColorType.Rgb },
{ TestImages.Png.Rgb48BppTrans, PngBitDepth.Bit16, PngColorType.Rgb }
};
/// <summary>
/// All types except Palette
/// </summary>
@ -249,6 +261,61 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
}
}
[Theory]
[MemberData(nameof(PngTrnsFiles))]
public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngColorType pngColorType)
{
var options = new PngEncoder();
var testFile = TestFile.Create(imagePath);
using (Image<Rgba32> input = testFile.CreateImage())
{
PngMetaData inMeta = input.MetaData.GetFormatMetaData(PngFormat.Instance);
Assert.True(inMeta.HasTrans);
using (var memStream = new MemoryStream())
{
input.Save(memStream, options);
memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream))
{
PngMetaData outMeta = output.MetaData.GetFormatMetaData(PngFormat.Instance);
Assert.True(outMeta.HasTrans);
switch (pngColorType)
{
case PngColorType.Grayscale:
if (pngBitDepth.Equals(PngBitDepth.Bit16))
{
Assert.True(outMeta.TransparentGray16.HasValue);
Assert.Equal(inMeta.TransparentGray16, outMeta.TransparentGray16);
}
else
{
Assert.True(outMeta.TransparentGray8.HasValue);
Assert.Equal(inMeta.TransparentGray8, outMeta.TransparentGray8);
}
break;
case PngColorType.Rgb:
if (pngBitDepth.Equals(PngBitDepth.Bit16))
{
Assert.True(outMeta.TransparentRgb48.HasValue);
Assert.Equal(inMeta.TransparentRgb48, outMeta.TransparentRgb48);
}
else
{
Assert.True(outMeta.TransparentRgb24.HasValue);
Assert.Equal(inMeta.TransparentRgb24, outMeta.TransparentRgb24);
}
break;
}
}
}
}
}
private static void TestPngEncoderCore<TPixel>(
TestImageProvider<TPixel> provider,
PngColorType pngColorType,

3
tests/ImageSharp.Tests/Helpers/ImageMathsTests.cs

@ -2,11 +2,10 @@
// Licensed under the Apache License, Version 2.0.
using System;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Helpers
{
using Xunit;
public class ImageMathsTests
{
[Theory]

168
tests/ImageSharp.Tests/Helpers/TolerantMathTests.cs

@ -0,0 +1,168 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using Xunit;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Helpers
{
public class TolerantMathTests
{
private readonly TolerantMath tolerantMath = new TolerantMath(0.1);
[Theory]
[InlineData(0)]
[InlineData(0.01)]
[InlineData(-0.05)]
public void IsZero_WhenTrue(double a)
{
Assert.True(this.tolerantMath.IsZero(a));
}
[Theory]
[InlineData(0.11)]
[InlineData(-0.101)]
[InlineData(42)]
public void IsZero_WhenFalse(double a)
{
Assert.False(this.tolerantMath.IsZero(a));
}
[Theory]
[InlineData(0.11)]
[InlineData(100)]
public void IsPositive_WhenTrue(double a)
{
Assert.True(this.tolerantMath.IsPositive(a));
}
[Theory]
[InlineData(0.09)]
[InlineData(-0.1)]
[InlineData(-1000)]
public void IsPositive_WhenFalse(double a)
{
Assert.False(this.tolerantMath.IsPositive(a));
}
[Theory]
[InlineData(-0.11)]
[InlineData(-100)]
public void IsNegative_WhenTrue(double a)
{
Assert.True(this.tolerantMath.IsNegative(a));
}
[Theory]
[InlineData(-0.09)]
[InlineData(0.1)]
[InlineData(1000)]
public void IsNegative_WhenFalse(double a)
{
Assert.False(this.tolerantMath.IsNegative(a));
}
[Theory]
[InlineData(4.2, 4.2)]
[InlineData(4.2, 4.25)]
[InlineData(-Math.PI, -Math.PI + 0.05)]
[InlineData(999999.2, 999999.25)]
public void AreEqual_WhenTrue(double a, double b)
{
Assert.True(this.tolerantMath.AreEqual(a, b));
}
[Theory]
[InlineData(1, 2)]
[InlineData(-1000000, -1000000.2)]
public void AreEqual_WhenFalse(double a, double b)
{
Assert.False(this.tolerantMath.AreEqual(a, b));
}
[Theory]
[InlineData(2, 1.8)]
[InlineData(-20, -20.2)]
[InlineData(0.1, -0.1)]
[InlineData(100, 10)]
public void IsGreater_IsLess_WhenTrue(double a, double b)
{
Assert.True(this.tolerantMath.IsGreater(a, b));
Assert.True(this.tolerantMath.IsLess(b, a));
}
[Theory]
[InlineData(2, 1.95)]
[InlineData(-20, -20.02)]
[InlineData(0.01, -0.01)]
[InlineData(999999, 999999.09)]
public void IsGreater_IsLess_WhenFalse(double a, double b)
{
Assert.False(this.tolerantMath.IsGreater(a, b));
Assert.False(this.tolerantMath.IsLess(b, a));
}
[Theory]
[InlineData(3, 2)]
[InlineData(3, 2.99)]
[InlineData(2.99, 3)]
[InlineData(-5, -6)]
[InlineData(-5, -5.05)]
[InlineData(-5.05, -5)]
public void IsGreaterOrEqual_IsLessOrEqual_WhenTrue(double a, double b)
{
Assert.True(this.tolerantMath.IsGreaterOrEqual(a, b));
Assert.True(this.tolerantMath.IsLessOrEqual(b, a));
}
[Theory]
[InlineData(2, 3)]
[InlineData(2.89, 3)]
[InlineData(-3, -2.89)]
public void IsGreaterOrEqual_IsLessOrEqual_WhenFalse(double a, double b)
{
Assert.False(this.tolerantMath.IsGreaterOrEqual(a, b));
Assert.False(this.tolerantMath.IsLessOrEqual(b, a));
}
[Theory]
[InlineData(3.5, 4.0)]
[InlineData(3.89, 4.0)]
[InlineData(4.09, 4.0)]
[InlineData(4.11, 5.0)]
[InlineData(0.11, 1)]
[InlineData(0.05, 0)]
[InlineData(-0.5, 0)]
[InlineData(-0.95, -1)]
[InlineData(-1.05, -1)]
[InlineData(-1.5, -1)]
public void Ceiling(double value, double expected)
{
double actual = this.tolerantMath.Ceiling(value);
Assert.Equal(expected, actual);
}
[Theory]
[InlineData(1, 1)]
[InlineData(0.99, 1)]
[InlineData(0.5, 0)]
[InlineData(0.01, 0)]
[InlineData(-0.09, 0)]
[InlineData(-0.11, -1)]
[InlineData(-100.11, -101)]
[InlineData(-100.09, -100)]
public void Floor(double value, double expected)
{
double plz1 = Math.IEEERemainder(1.1, 1);
double plz2 = Math.IEEERemainder(0.9, 1);
double plz3 = Math.IEEERemainder(-1.1, 1);
double plz4 = Math.IEEERemainder(-0.9, 1);
double actual = this.tolerantMath.Floor(value);
Assert.Equal(expected, actual);
}
}
}

61
tests/ImageSharp.Tests/Processing/Processors/Transforms/KernelMapTests.cs

@ -1,61 +0,0 @@
using System;
using System.IO;
using System.Text;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives;
using Xunit;
using Xunit.Abstractions;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
public class KernelMapTests
{
private ITestOutputHelper Output { get; }
public KernelMapTests(ITestOutputHelper output)
{
this.Output = output;
}
[Theory(Skip = "TODO: Add asserionts")]
[InlineData(500, 200, nameof(KnownResamplers.Bicubic))]
[InlineData(50, 40, nameof(KnownResamplers.Bicubic))]
[InlineData(40, 30, nameof(KnownResamplers.Bicubic))]
[InlineData(500, 200, nameof(KnownResamplers.Lanczos8))]
[InlineData(100, 80, nameof(KnownResamplers.Lanczos8))]
[InlineData(100, 10, nameof(KnownResamplers.Lanczos8))]
[InlineData(10, 100, nameof(KnownResamplers.Lanczos8))]
public void PrintKernelMap(int srcSize, int destSize, string resamplerName)
{
var resampler = (IResampler)typeof(KnownResamplers).GetProperty(resamplerName).GetValue(null);
var kernelMap = KernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator);
var bld = new StringBuilder();
foreach (ResizeKernel window in kernelMap.Kernels)
{
Span<float> span = window.GetSpan();
for (int i = 0; i < window.Length; i++)
{
float value = span[i];
bld.Append($"{value,7:F4}");
bld.Append("| ");
}
bld.AppendLine();
}
string outDir = TestEnvironment.CreateOutputDirectory("." + nameof(this.PrintKernelMap));
string fileName = $@"{outDir}\{resamplerName}_{srcSize}_{destSize}.MD";
File.WriteAllText(fileName, bld.ToString());
this.Output.WriteLine(bld.ToString());
}
}
}

111
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs

@ -0,0 +1,111 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
using System.Linq;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
public partial class ResizeKernelMapTests
{
/// <summary>
/// Simplified reference implementation for <see cref="ResizeKernelMap"/> functionality.
/// </summary>
internal class ReferenceKernelMap
{
private readonly ReferenceKernel[] kernels;
public ReferenceKernelMap(ReferenceKernel[] kernels)
{
this.kernels = kernels;
}
public int DestinationSize => this.kernels.Length;
public ReferenceKernel GetKernel(int destinationIndex) => this.kernels[destinationIndex];
public static ReferenceKernelMap Calculate(IResampler sampler, int destinationSize, int sourceSize, bool normalize = true)
{
double ratio = (double)sourceSize / destinationSize;
double scale = ratio;
if (scale < 1F)
{
scale = 1F;
}
TolerantMath tolerantMath = TolerantMath.Default;
double radius = tolerantMath.Ceiling(scale * sampler.Radius);
var result = new List<ReferenceKernel>();
for (int i = 0; i < destinationSize; i++)
{
double center = ((i + .5) * ratio) - .5;
// Keep inside bounds.
int left = (int)tolerantMath.Ceiling(center - radius);
if (left < 0)
{
left = 0;
}
int right = (int)tolerantMath.Floor(center + radius);
if (right > sourceSize - 1)
{
right = sourceSize - 1;
}
double sum = 0;
double[] values = new double[right - left + 1];
for (int j = left; j <= right; j++)
{
double weight = sampler.GetValue((float)((j - center) / scale));
sum += weight;
values[j - left] = weight;
}
if (sum > 0 && normalize)
{
for (int w = 0; w < values.Length; w++)
{
values[w] /= sum;
}
}
float[] floatVals = values.Select(v => (float)v).ToArray();
result.Add(new ReferenceKernel(left, floatVals));
}
return new ReferenceKernelMap(result.ToArray());
}
}
internal struct ReferenceKernel
{
public ReferenceKernel(int left, float[] values)
{
this.Left = left;
this.Values = values;
}
public int Left { get; }
public float[] Values { get; }
public int Length => this.Values.Length;
public static implicit operator ReferenceKernel(ResizeKernel orig)
{
return new ReferenceKernel(orig.Left, orig.Values.ToArray());
}
}
}
}

241
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs

@ -0,0 +1,241 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using Xunit;
using Xunit.Abstractions;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
public partial class ResizeKernelMapTests
{
private ITestOutputHelper Output { get; }
public ResizeKernelMapTests(ITestOutputHelper output)
{
this.Output = output;
}
/// <summary>
/// resamplerName, srcSize, destSize
/// </summary>
public static readonly TheoryData<string, int, int> KernelMapData = new TheoryData<string, int, int>
{
{ nameof(KnownResamplers.Bicubic), 15, 10 },
{ nameof(KnownResamplers.Bicubic), 10, 15 },
{ nameof(KnownResamplers.Bicubic), 20, 20 },
{ nameof(KnownResamplers.Bicubic), 50, 40 },
{ nameof(KnownResamplers.Bicubic), 40, 50 },
{ nameof(KnownResamplers.Bicubic), 500, 200 },
{ nameof(KnownResamplers.Bicubic), 200, 500 },
{ nameof(KnownResamplers.Bicubic), 10, 25 },
{ nameof(KnownResamplers.Lanczos3), 16, 12 },
{ nameof(KnownResamplers.Lanczos3), 12, 16 },
{ nameof(KnownResamplers.Lanczos3), 12, 9 },
{ nameof(KnownResamplers.Lanczos3), 9, 12 },
{ nameof(KnownResamplers.Lanczos3), 6, 8 },
{ nameof(KnownResamplers.Lanczos3), 8, 6 },
{ nameof(KnownResamplers.Lanczos3), 20, 12 },
{ nameof(KnownResamplers.Lanczos3), 5, 25 },
{ nameof(KnownResamplers.Lanczos3), 5, 50 },
{ nameof(KnownResamplers.Lanczos3), 25, 5 },
{ nameof(KnownResamplers.Lanczos3), 50, 5 },
{ nameof(KnownResamplers.Lanczos3), 49, 5 },
{ nameof(KnownResamplers.Lanczos3), 31, 5 },
{ nameof(KnownResamplers.Lanczos8), 500, 200 },
{ nameof(KnownResamplers.Lanczos8), 100, 10 },
{ nameof(KnownResamplers.Lanczos8), 100, 80 },
{ nameof(KnownResamplers.Lanczos8), 10, 100 },
// Resize_WorksWithAllResamplers_Rgba32_CalliphoraPartial_Box-0.5:
{ nameof(KnownResamplers.Box), 378, 149 },
{ nameof(KnownResamplers.Box), 349, 174 },
// Accuracy-related regression-test cases cherry-picked from GeneratedImageResizeData
{ nameof(KnownResamplers.Box), 201, 100 },
{ nameof(KnownResamplers.Box), 199, 99 },
{ nameof(KnownResamplers.Box), 10, 299 },
{ nameof(KnownResamplers.Box), 299, 10 },
{ nameof(KnownResamplers.Box), 301, 300 },
{ nameof(KnownResamplers.Box), 1180, 480 },
{ nameof(KnownResamplers.Lanczos2), 3264, 3032 },
{ nameof(KnownResamplers.Bicubic), 1280, 2240 },
{ nameof(KnownResamplers.Bicubic), 1920, 1680 },
{ nameof(KnownResamplers.Bicubic), 3072, 2240 },
{ nameof(KnownResamplers.Welch), 300, 2008 },
// ResizeKernel.Length -related regression tests cherry-picked from GeneratedImageResizeData
{ nameof(KnownResamplers.Bicubic), 10, 50 },
{ nameof(KnownResamplers.Bicubic), 49, 301 },
{ nameof(KnownResamplers.Bicubic), 301, 49 },
{ nameof(KnownResamplers.Bicubic), 1680, 1200 },
{ nameof(KnownResamplers.Box), 13, 299 },
{ nameof(KnownResamplers.Lanczos5), 3032, 600 },
};
public static TheoryData<string, int, int> GeneratedImageResizeData =
GenerateImageResizeData();
[Theory(Skip = "Only for debugging and development")]
[MemberData(nameof(KernelMapData))]
public void PrintNonNormalizedKernelMap(string resamplerName, int srcSize, int destSize)
{
IResampler resampler = TestUtils.GetResampler(resamplerName);
var kernelMap = ReferenceKernelMap.Calculate(resampler, destSize, srcSize, false);
this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n");
}
[Theory]
[MemberData(nameof(KernelMapData))]
public void KernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize)
{
this.VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize);
}
// Comprehensive but expensive tests, for ResizeKernelMap.
// Enabling them can kill you, but sometimes you have to wear the burden!
// AppVeyor will never follow you to these shadows of Mordor.
#if false
[Theory]
[MemberData(nameof(GeneratedImageResizeData))]
public void KernelMapContentIsCorrect_ExtendedGeneratedValues(string resamplerName, int srcSize, int destSize)
{
this.VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize);
}
#endif
private void VerifyKernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize)
{
IResampler resampler = TestUtils.GetResampler(resamplerName);
var referenceMap = ReferenceKernelMap.Calculate(resampler, destSize, srcSize);
var kernelMap = ResizeKernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator);
#if DEBUG
this.Output.WriteLine($"Expected KernelMap:\n{PrintKernelMap(referenceMap)}\n");
this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n");
#endif
var comparer = new ApproximateFloatComparer(1e-6f);
for (int i = 0; i < kernelMap.DestinationLength; i++)
{
ResizeKernel kernel = kernelMap.GetKernel(i);
ReferenceKernel referenceKernel = referenceMap.GetKernel(i);
Assert.True(
referenceKernel.Length == kernel.Length,
$"referenceKernel.Length != kernel.Length: {referenceKernel.Length} != {kernel.Length}");
Assert.True(
referenceKernel.Left == kernel.Left,
$"referenceKernel.Left != kernel.Left: {referenceKernel.Left} != {kernel.Left}");
float[] expectedValues = referenceKernel.Values;
Span<float> actualValues = kernel.Values;
Assert.Equal(expectedValues.Length, actualValues.Length);
for (int x = 0; x < expectedValues.Length; x++)
{
Assert.True(
comparer.Equals(expectedValues[x], actualValues[x]),
$"{expectedValues[x]} != {actualValues[x]} @ (Row:{i}, Col:{x})");
}
}
}
private static string PrintKernelMap(ResizeKernelMap kernelMap) =>
PrintKernelMap(kernelMap, km => km.DestinationLength, (km, i) => km.GetKernel(i));
private static string PrintKernelMap(ReferenceKernelMap kernelMap) =>
PrintKernelMap(kernelMap, km => km.DestinationSize, (km, i) => km.GetKernel(i));
private static string PrintKernelMap<TKernelMap>(
TKernelMap kernelMap,
Func<TKernelMap, int> getDestinationSize,
Func<TKernelMap, int, ReferenceKernel> getKernel)
{
var bld = new StringBuilder();
if (kernelMap is ResizeKernelMap actualMap)
{
bld.AppendLine(actualMap.Info);
}
int destinationSize = getDestinationSize(kernelMap);
for (int i = 0; i < destinationSize; i++)
{
ReferenceKernel kernel = getKernel(kernelMap, i);
bld.Append($"[{i:D3}] (L{kernel.Left:D3}) || ");
Span<float> span = kernel.Values;
for (int j = 0; j < kernel.Length; j++)
{
float value = span[j];
bld.Append($"{value,8:F5}");
bld.Append(" | ");
}
bld.AppendLine();
}
return bld.ToString();
}
private static TheoryData<string, int, int> GenerateImageResizeData()
{
var result = new TheoryData<string, int, int>();
string[] resamplerNames = typeof(KnownResamplers).GetProperties(BindingFlags.Public | BindingFlags.Static)
.Select(p => p.Name)
.Where(name => name != nameof(KnownResamplers.NearestNeighbor))
.ToArray();
int[] dimensionVals =
{
// Arbitrary, small dimensions:
9, 10, 11, 13, 49, 50, 53, 99, 100, 199, 200, 201, 299, 300, 301,
// Typical image sizes:
640, 480, 800, 600, 1024, 768, 1280, 960, 1536, 1180, 1600, 1200, 2048, 1536, 2240, 1680, 2560,
1920, 3032, 2008, 3072, 2304, 3264, 2448
};
IOrderedEnumerable<(int s, int d)> source2Dest = dimensionVals
.SelectMany(s => dimensionVals.Select(d => (s, d)))
.OrderBy(x => x.s + x.d);
foreach (string resampler in resamplerNames)
{
foreach ((int s, int d) x in source2Dest)
{
result.Add(resampler, x.s, x.d);
}
}
return result;
}
}
}

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

@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Primitives;
using Xunit;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
@ -20,7 +21,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.07F);
public static readonly TheoryData<string, IResampler> AllReSamplers =
public static readonly TheoryData<string, IResampler> AllResamplers =
new TheoryData<string, IResampler>
{
{ "Bicubic", KnownResamplers.Bicubic },
@ -40,9 +41,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
};
[Theory]
[WithTestPatternImages(nameof(AllReSamplers), 100, 100, DefaultPixelType, 0.5f)]
[WithFileCollection(nameof(CommonTestImages), nameof(AllReSamplers), DefaultPixelType, 0.5f)]
[WithFileCollection(nameof(CommonTestImages), nameof(AllReSamplers), DefaultPixelType, 0.3f)]
[WithTestPatternImages(nameof(AllResamplers), 100, 100, DefaultPixelType, 0.5f)]
[WithFileCollection(nameof(CommonTestImages), nameof(AllResamplers), DefaultPixelType, 0.5f)]
[WithFileCollection(nameof(CommonTestImages), nameof(AllResamplers), DefaultPixelType, 0.3f)]
public void Resize_WorksWithAllResamplers<TPixel>(TestImageProvider<TPixel> provider, string name, IResampler sampler, float ratio)
where TPixel : struct, IPixel<TPixel>
{

14
tests/ImageSharp.Tests/Processing/Processors/Transforms/SkewTest.cs

@ -60,7 +60,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
foreach (string resamplerName in ResamplerNames)
{
IResampler sampler = GetResampler(resamplerName);
IResampler sampler = TestUtils.GetResampler(resamplerName);
using (Image<TPixel> image = provider.GetImage())
{
image.Mutate(i => i.Skew(x, y, sampler));
@ -68,17 +68,5 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
}
}
}
private static IResampler GetResampler(string name)
{
PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name);
if (property is null)
{
throw new Exception($"No resampler named '{name}");
}
return (IResampler)property.GetValue(null);
}
}
}

71
tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs

@ -0,0 +1,71 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using SixLabors.ImageSharp.Processing;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Tests.Processing.Transforms
{
public class AffineTransformBuilderTests : TransformBuilderTestBase<AffineTransformBuilder>
{
protected override AffineTransformBuilder CreateBuilder(Rectangle rectangle) => new AffineTransformBuilder();
protected override void AppendRotationDegrees(AffineTransformBuilder builder, float degrees)
=> builder.AppendRotationDegrees(degrees);
protected override void AppendRotationDegrees(AffineTransformBuilder builder, float degrees, Vector2 origin)
=> builder.AppendRotationDegrees(degrees, origin);
protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians)
=> builder.AppendRotationRadians(radians);
protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians, Vector2 origin)
=> builder.AppendRotationRadians(radians, origin);
protected override void AppendScale(AffineTransformBuilder builder, SizeF scale)
=> builder.AppendScale(scale);
protected override void AppendSkewDegrees(AffineTransformBuilder builder, float degreesX, float degreesY)
=> builder.AppendSkewDegrees(degreesX, degreesY);
protected override void AppendSkewDegrees(AffineTransformBuilder builder, float degreesX, float degreesY, Vector2 origin)
=> builder.AppendSkewDegrees(degreesX, degreesY, origin);
protected override void AppendSkewRadians(AffineTransformBuilder builder, float radiansX, float radiansY)
=> builder.AppendSkewRadians(radiansX, radiansY);
protected override void AppendSkewRadians(AffineTransformBuilder builder, float radiansX, float radiansY, Vector2 origin)
=> builder.AppendSkewRadians(radiansX, radiansY, origin);
protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate)
=> builder.AppendTranslation(translate);
protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians)
=> builder.PrependRotationRadians(radians);
protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians, Vector2 origin)
=> builder.PrependRotationRadians(radians, origin);
protected override void PrependScale(AffineTransformBuilder builder, SizeF scale)
=> builder.PrependScale(scale);
protected override void PrependSkewRadians(AffineTransformBuilder builder, float radiansX, float radiansY)
=> builder.PrependSkewRadians(radiansX, radiansY);
protected override void PrependSkewRadians(AffineTransformBuilder builder, float radiansX, float radiansY, Vector2 origin)
=> builder.PrependSkewRadians(radiansX, radiansY, origin);
protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate)
=> builder.PrependTranslation(translate);
protected override Vector2 Execute(
AffineTransformBuilder builder,
Rectangle rectangle,
Vector2 sourcePoint)
{
Matrix3x2 matrix = builder.BuildMatrix(rectangle);
return Vector2.Transform(sourcePoint, matrix);
}
}
}

72
tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs

@ -17,7 +17,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
{
private readonly ITestOutputHelper Output;
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0085f, 3);
// 1 byte difference on one color component.
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0134F, 3);
/// <summary>
/// angleDeg, sx, sy, tx, ty
@ -78,15 +79,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
IResampler resampler = GetResampler(resamplerName);
using (Image<TPixel> image = provider.GetImage())
{
var rotate = Matrix3x2.CreateRotation((float)Math.PI / 4F, new Vector2(5 / 2F, 5 / 2F));
var translate = Matrix3x2.CreateTranslation((7 - 5) / 2F, (7 - 5) / 2F);
AffineTransformBuilder builder = new AffineTransformBuilder()
.AppendRotationDegrees(30);
Rectangle sourceRectangle = image.Bounds();
Matrix3x2 matrix = rotate * translate;
Rectangle destRectangle = TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, matrix);
image.Mutate(c => c.Transform(matrix, resampler, destRectangle));
image.Mutate(c => c.Transform(builder, resampler));
image.DebugSave(provider, resamplerName);
VerifyAllPixelsAreWhiteOrTransparent(image);
@ -104,14 +100,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
{
using (Image<TPixel> image = provider.GetImage())
{
Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(angleDeg);
var translate = Matrix3x2.CreateTranslation(tx, ty);
var scale = Matrix3x2.CreateScale(sx, sy);
Matrix3x2 m = rotate * scale * translate;
image.DebugSave(provider, $"_original");
AffineTransformBuilder builder = new AffineTransformBuilder()
.AppendRotationDegrees(angleDeg)
.AppendScale(new SizeF(sx, sy))
.AppendTranslation(new PointF(tx, ty));
this.PrintMatrix(m);
this.PrintMatrix(builder.BuildMatrix(image.Size()));
image.Mutate(i => i.Transform(m, KnownResamplers.Bicubic));
image.Mutate(i => i.Transform(builder, KnownResamplers.Bicubic));
FormattableString testOutputDetails = $"R({angleDeg})_S({sx},{sy})_T({tx},{ty})";
image.DebugSave(provider, testOutputDetails);
@ -126,9 +123,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
{
using (Image<TPixel> image = provider.GetImage())
{
Matrix3x2 m = this.MakeManuallyCenteredMatrix(angleDeg, s, image);
AffineTransformBuilder builder = new AffineTransformBuilder()
.AppendRotationDegrees(angleDeg)
.AppendScale(new SizeF(s, s));
image.Mutate(i => i.Transform(m, KnownResamplers.Bicubic));
image.Mutate(i => i.Transform(builder, KnownResamplers.Bicubic));
FormattableString testOutputDetails = $"R({angleDeg})_S({s})";
image.DebugSave(provider, testOutputDetails);
@ -155,13 +154,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
public void Transform_FromSourceRectangle1<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
var rectangle = new Rectangle(48, 0, 96, 36);
var rectangle = new Rectangle(48, 0, 48, 24);
using (Image<TPixel> image = provider.GetImage())
{
var m = Matrix3x2.CreateScale(2.0F, 1.5F);
image.DebugSave(provider, $"_original");
AffineTransformBuilder builder = new AffineTransformBuilder()
.AppendScale(new SizeF(2, 1.5F));
image.Mutate(i => i.Transform(m, KnownResamplers.Spline, rectangle));
image.Mutate(i => i.Transform(rectangle, builder, KnownResamplers.Spline));
image.DebugSave(provider);
image.CompareToReferenceOutput(ValidatorComparer, provider);
@ -173,13 +174,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
public void Transform_FromSourceRectangle2<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
var rectangle = new Rectangle(0, 24, 48, 48);
var rectangle = new Rectangle(0, 24, 48, 24);
using (Image<TPixel> image = provider.GetImage())
{
var m = Matrix3x2.CreateScale(1.0F, 2.0F);
AffineTransformBuilder builder = new AffineTransformBuilder()
.AppendScale(new SizeF(1F, 2F));
image.Mutate(i => i.Transform(m, KnownResamplers.Spline, rectangle));
image.Mutate(i => i.Transform(rectangle, builder, KnownResamplers.Spline));
image.DebugSave(provider);
image.CompareToReferenceOutput(ValidatorComparer, provider);
@ -194,33 +196,17 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
IResampler sampler = GetResampler(resamplerName);
using (Image<TPixel> image = provider.GetImage())
{
Matrix3x2 m = this.MakeManuallyCenteredMatrix(50, 0.6f, image);
AffineTransformBuilder builder = new AffineTransformBuilder()
.AppendRotationDegrees(50)
.AppendScale(new SizeF(.6F, .6F));
image.Mutate(i =>
{
i.Transform(m, sampler);
});
image.Mutate(i => i.Transform(builder, sampler));
image.DebugSave(provider, resamplerName);
image.CompareToReferenceOutput(ValidatorComparer, provider, resamplerName);
}
}
private Matrix3x2 MakeManuallyCenteredMatrix<TPixel>(float angleDeg, float s, Image<TPixel> image)
where TPixel : struct, IPixel<TPixel>
{
Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(angleDeg);
Vector2 toCenter = 0.5f * new Vector2(image.Width, image.Height);
var translate = Matrix3x2.CreateTranslation(-toCenter);
var translateBack = Matrix3x2.CreateTranslation(toCenter);
var scale = Matrix3x2.CreateScale(s);
Matrix3x2 m = translate * rotate * scale * translateBack;
this.PrintMatrix(m);
return m;
}
private static IResampler GetResampler(string name)
{
PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name);

62
tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs

@ -0,0 +1,62 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using SixLabors.ImageSharp.Processing;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Tests.Processing.Transforms
{
public class ProjectiveTransformBuilderTests : TransformBuilderTestBase<ProjectiveTransformBuilder>
{
protected override ProjectiveTransformBuilder CreateBuilder(Rectangle rectangle) => new ProjectiveTransformBuilder();
protected override void AppendRotationDegrees(ProjectiveTransformBuilder builder, float degrees) => builder.AppendRotationDegrees(degrees);
protected override void AppendRotationDegrees(ProjectiveTransformBuilder builder, float degrees, Vector2 origin) => builder.AppendRotationDegrees(degrees, origin);
protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.AppendRotationRadians(radians);
protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 origin) => builder.AppendRotationRadians(radians, origin);
protected override void AppendScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.AppendScale(scale);
protected override void AppendSkewDegrees(ProjectiveTransformBuilder builder, float degreesX, float degreesY)
=> builder.AppendSkewDegrees(degreesX, degreesY);
protected override void AppendSkewDegrees(ProjectiveTransformBuilder builder, float degreesX, float degreesY, Vector2 origin)
=> builder.AppendSkewDegrees(degreesX, degreesY, origin);
protected override void AppendSkewRadians(ProjectiveTransformBuilder builder, float radiansX, float radiansY)
=> builder.AppendSkewRadians(radiansX, radiansY);
protected override void AppendSkewRadians(ProjectiveTransformBuilder builder, float radiansX, float radiansY, Vector2 origin)
=> builder.AppendSkewRadians(radiansX, radiansY, origin);
protected override void AppendTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate);
protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.PrependRotationRadians(radians);
protected override void PrependScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.PrependScale(scale);
protected override void PrependSkewRadians(ProjectiveTransformBuilder builder, float radiansX, float radiansY)
=> builder.PrependSkewRadians(radiansX, radiansY);
protected override void PrependSkewRadians(ProjectiveTransformBuilder builder, float radiansX, float radiansY, Vector2 origin)
=> builder.PrependSkewRadians(radiansX, radiansY, origin);
protected override void PrependTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.PrependTranslation(translate);
protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 origin) =>
builder.PrependRotationRadians(radians, origin);
protected override Vector2 Execute(
ProjectiveTransformBuilder builder,
Rectangle rectangle,
Vector2 sourcePoint)
{
Matrix4x4 matrix = builder.BuildMatrix(rectangle);
return Vector2.Transform(sourcePoint, matrix);
}
}
}

27
tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs

@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
private static readonly ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.5f, 3);
private ITestOutputHelper Output { get; }
public static readonly TheoryData<string> ResamplerNames = new TheoryData<string>
{
nameof(KnownResamplers.Bicubic),
@ -60,10 +60,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
};
public ProjectiveTransformTests(ITestOutputHelper output)
{
this.Output = output;
}
public ProjectiveTransformTests(ITestOutputHelper output) => this.Output = output;
[Theory]
[WithTestPatternImages(nameof(ResamplerNames), 150, 150, PixelTypes.Rgba32)]
@ -73,9 +70,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
IResampler sampler = GetResampler(resamplerName);
using (Image<TPixel> image = provider.GetImage())
{
Matrix4x4 m = ProjectiveTransformHelper.CreateTaperMatrix(image.Size(), TaperSide.Right, TaperCorner.Both, .5F);
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder()
.AppendTaper(TaperSide.Right, TaperCorner.Both, .5F);
image.Mutate(i => { i.Transform(m, sampler); });
image.Mutate(i => i.Transform(builder, sampler));
image.DebugSave(provider, resamplerName);
image.CompareToReferenceOutput(ValidatorComparer, provider, resamplerName);
@ -89,8 +87,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
{
using (Image<TPixel> image = provider.GetImage())
{
Matrix4x4 m = ProjectiveTransformHelper.CreateTaperMatrix(image.Size(), taperSide, taperCorner, .5F);
image.Mutate(i => { i.Transform(m); });
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder()
.AppendTaper(taperSide, taperCorner, .5F);
image.Mutate(i => i.Transform(builder));
FormattableString testOutputDetails = $"{taperSide}-{taperCorner}";
image.DebugSave(provider, testOutputDetails);
@ -110,10 +110,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
// https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/transforms/non-affine
using (Image<TPixel> image = provider.GetImage())
{
Matrix4x4 m = Matrix4x4.Identity;
m.M13 = 0.01F;
Matrix4x4 matrix = Matrix4x4.Identity;
matrix.M13 = 0.01F;
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder()
.AppendMatrix(matrix);
image.Mutate(i => { i.Transform(m); });
image.Mutate(i => i.Transform(builder));
image.DebugSave(provider);
image.CompareToReferenceOutput(TolerantComparer, provider);

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

@ -0,0 +1,275 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Transforms
{
public abstract class TransformBuilderTestBase<TBuilder>
{
private static readonly ApproximateFloatComparer Comparer = new ApproximateFloatComparer(1e-6f);
public static readonly TheoryData<Vector2, Vector2, Vector2, Vector2> ScaleTranslate_Data =
new TheoryData<Vector2, Vector2, Vector2, Vector2>
{
// scale, translate, source, expectedDest
{ Vector2.One, Vector2.Zero, Vector2.Zero, Vector2.Zero },
{ Vector2.One, Vector2.Zero, new Vector2(10, 20), new Vector2(10, 20) },
{ Vector2.One, new Vector2(3, 1), new Vector2(10, 20), new Vector2(13, 21) },
{ new Vector2(2, 0.5f), new Vector2(3, 1), new Vector2(10, 20), new Vector2(23, 11) },
};
[Theory]
[MemberData(nameof(ScaleTranslate_Data))]
public void _1Scale_2Translate(Vector2 scale, Vector2 translate, Vector2 source, Vector2 expectedDest)
{
// These operations should be size-agnostic:
var size = new Size(123, 321);
TBuilder builder = this.CreateBuilder(size);
this.AppendScale(builder, new SizeF(scale));
this.AppendTranslation(builder, translate);
Vector2 actualDest = this.Execute(builder, new Rectangle(Point.Empty, size), source);
Assert.True(Comparer.Equals(expectedDest, actualDest));
}
public static readonly TheoryData<Vector2, Vector2, Vector2, Vector2> TranslateScale_Data =
new TheoryData<Vector2, Vector2, Vector2, Vector2>
{
// translate, scale, source, expectedDest
{ Vector2.Zero, Vector2.One, Vector2.Zero, Vector2.Zero },
{ Vector2.Zero, Vector2.One, new Vector2(10, 20), new Vector2(10, 20) },
{ new Vector2(3, 1), new Vector2(2, 0.5f), new Vector2(10, 20), new Vector2(26, 10.5f) },
};
[Theory]
[MemberData(nameof(TranslateScale_Data))]
public void _1Translate_2Scale(Vector2 translate, Vector2 scale, Vector2 source, Vector2 expectedDest)
{
// Translate ans scale are size-agnostic:
var size = new Size(456, 432);
TBuilder builder = this.CreateBuilder(size);
this.AppendTranslation(builder, translate);
this.AppendScale(builder, new SizeF(scale));
Vector2 actualDest = this.Execute(builder, new Rectangle(Point.Empty, size), source);
Assert.Equal(expectedDest, actualDest, Comparer);
}
[Theory]
[InlineData(10, 20)]
[InlineData(-20, 10)]
public void LocationOffsetIsPrepended(int locationX, int locationY)
{
var rectangle = new Rectangle(locationX, locationY, 10, 10);
TBuilder builder = this.CreateBuilder(rectangle);
this.AppendScale(builder, new SizeF(2, 2));
Vector2 actual = this.Execute(builder, rectangle, Vector2.One);
Vector2 expected = new Vector2(-locationX + 1, -locationY + 1) * 2;
Assert.Equal(actual, expected, Comparer);
}
[Theory]
[InlineData(200, 100, 10, 42, 84)]
[InlineData(200, 100, 100, 42, 84)]
[InlineData(100, 200, -10, 42, 84)]
public void AppendRotationDegrees_WithoutSpecificRotationCenter_RotationIsCenteredAroundImageCenter(
int width,
int height,
float degrees,
float x,
float y)
{
var size = new Size(width, height);
TBuilder builder = this.CreateBuilder(size);
this.AppendRotationDegrees(builder, degrees);
// TODO: We should also test CreateRotationMatrixDegrees() (and all TransformUtils stuff!) for correctness
Matrix3x2 matrix = TransformUtils.CreateRotationMatrixDegrees(degrees, size);
var position = new Vector2(x, y);
var expected = Vector2.Transform(position, matrix);
Vector2 actual = this.Execute(builder, new Rectangle(Point.Empty, size), position);
Assert.Equal(actual, expected, Comparer);
}
[Theory]
[InlineData(200, 100, 10, 30, 61, 42, 84)]
[InlineData(200, 100, 100, 30, 10, 20, 84)]
[InlineData(100, 200, -10, 30, 20, 11, 84)]
public void AppendRotationDegrees_WithRotationCenter(
int width,
int height,
float degrees,
float cx,
float cy,
float x,
float y)
{
var size = new Size(width, height);
TBuilder builder = this.CreateBuilder(size);
var centerPoint = new Vector2(cx, cy);
this.AppendRotationDegrees(builder, degrees, centerPoint);
var matrix = Matrix3x2.CreateRotation(GeometryUtilities.DegreeToRadian(degrees), centerPoint);
var position = new Vector2(x, y);
var expected = Vector2.Transform(position, matrix);
Vector2 actual = this.Execute(builder, new Rectangle(Point.Empty, size), position);
Assert.Equal(actual, expected, Comparer);
}
[Theory]
[InlineData(200, 100, 10, 10, 42, 84)]
[InlineData(200, 100, 100, 100, 42, 84)]
[InlineData(100, 200, -10, -10, 42, 84)]
public void AppendSkewDegrees_WithoutSpecificSkewCenter_SkewIsCenteredAroundImageCenter(
int width,
int height,
float degreesX,
float degreesY,
float x,
float y)
{
var size = new Size(width, height);
TBuilder builder = this.CreateBuilder(size);
this.AppendSkewDegrees(builder, degreesX, degreesY);
Matrix3x2 matrix = TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, size);
var position = new Vector2(x, y);
var expected = Vector2.Transform(position, matrix);
Vector2 actual = this.Execute(builder, new Rectangle(Point.Empty, size), position);
Assert.Equal(actual, expected, Comparer);
}
[Theory]
[InlineData(200, 100, 10, 10, 30, 61, 42, 84)]
[InlineData(200, 100, 100, 100, 30, 10, 20, 84)]
[InlineData(100, 200, -10, -10, 30, 20, 11, 84)]
public void AppendSkewDegrees_WithSkewCenter(
int width,
int height,
float degreesX,
float degreesY,
float cx,
float cy,
float x,
float y)
{
var size = new Size(width, height);
TBuilder builder = this.CreateBuilder(size);
var centerPoint = new Vector2(cx, cy);
this.AppendSkewDegrees(builder, degreesX, degreesY, centerPoint);
var matrix = Matrix3x2.CreateSkew(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), centerPoint);
var position = new Vector2(x, y);
var expected = Vector2.Transform(position, matrix);
Vector2 actual = this.Execute(builder, new Rectangle(Point.Empty, size), position);
Assert.Equal(actual, expected, Comparer);
}
[Fact]
public void AppendPrependOpposite()
{
var rectangle = new Rectangle(-1, -1, 3, 3);
TBuilder b1 = this.CreateBuilder(rectangle);
TBuilder b2 = this.CreateBuilder(rectangle);
const float pi = (float)Math.PI;
// Forwards
this.AppendRotationRadians(b1, pi);
this.AppendSkewRadians(b1, pi, pi);
this.AppendScale(b1, new SizeF(2, 0.5f));
this.AppendRotationRadians(b1, pi / 2, new Vector2(-0.5f, -0.1f));
this.AppendSkewRadians(b1, pi, pi / 2, new Vector2(-0.5f, -0.1f));
this.AppendTranslation(b1, new PointF(123, 321));
// Backwards
this.PrependTranslation(b2, new PointF(123, 321));
this.PrependSkewRadians(b2, pi, pi / 2, new Vector2(-0.5f, -0.1f));
this.PrependRotationRadians(b2, pi / 2, new Vector2(-0.5f, -0.1f));
this.PrependScale(b2, new SizeF(2, 0.5f));
this.PrependSkewRadians(b2, pi, pi);
this.PrependRotationRadians(b2, pi);
Vector2 p1 = this.Execute(b1, rectangle, new Vector2(32, 65));
Vector2 p2 = this.Execute(b2, rectangle, new Vector2(32, 65));
Assert.Equal(p1, p2, Comparer);
}
[Theory]
[InlineData(0, 1)]
[InlineData(1, 0)]
[InlineData(-1, 0)]
public void ThrowsForInvalidSizes(int width, int height)
{
var size = new Size(width, height);
Assert.ThrowsAny<ArgumentOutOfRangeException>(
() =>
{
TBuilder builder = this.CreateBuilder(size);
this.Execute(builder, new Rectangle(Point.Empty, size), Vector2.Zero);
});
}
protected TBuilder CreateBuilder(Size size) => this.CreateBuilder(new Rectangle(Point.Empty, size));
protected abstract TBuilder CreateBuilder(Rectangle rectangle);
protected abstract void AppendRotationDegrees(TBuilder builder, float degrees);
protected abstract void AppendRotationDegrees(TBuilder builder, float degrees, Vector2 origin);
protected abstract void AppendRotationRadians(TBuilder builder, float radians);
protected abstract void AppendRotationRadians(TBuilder builder, float radians, Vector2 origin);
protected abstract void AppendScale(TBuilder builder, SizeF scale);
protected abstract void AppendSkewDegrees(TBuilder builder, float degreesX, float degreesY);
protected abstract void AppendSkewDegrees(TBuilder builder, float degreesX, float degreesY, Vector2 origin);
protected abstract void AppendSkewRadians(TBuilder builder, float radiansX, float radiansY);
protected abstract void AppendSkewRadians(TBuilder builder, float radiansX, float radiansY, Vector2 origin);
protected abstract void AppendTranslation(TBuilder builder, PointF translate);
protected abstract void PrependRotationRadians(TBuilder builder, float radians);
protected abstract void PrependRotationRadians(TBuilder builder, float radians, Vector2 origin);
protected abstract void PrependScale(TBuilder builder, SizeF scale);
protected abstract void PrependSkewRadians(TBuilder builder, float radiansX, float radiansY);
protected abstract void PrependSkewRadians(TBuilder builder, float radiansX, float radiansY, Vector2 origin);
protected abstract void PrependTranslation(TBuilder builder, PointF translate);
protected abstract Vector2 Execute(TBuilder builder, Rectangle rectangle, Vector2 sourcePoint);
}
}

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

@ -25,7 +25,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
Assert.Equal(ExifDataType.Long, profile.GetValue(ExifTag.PixelXDimension).DataType);
Assert.Equal(ExifDataType.Long, profile.GetValue(ExifTag.PixelYDimension).DataType);
TransformHelpers.UpdateDimensionalMetData(img);
TransformProcessorHelpers.UpdateDimensionalMetData(img);
Assert.Equal(ExifDataType.Short, profile.GetValue(ExifTag.PixelXDimension).DataType);
Assert.Equal(ExifDataType.Short, profile.GetValue(ExifTag.PixelYDimension).DataType);

4
tests/ImageSharp.Tests/ProfilingBenchmarks/JpegBenchmarks.cs → tests/ImageSharp.Tests/ProfilingBenchmarks/JpegProfilingBenchmarks.cs

@ -15,9 +15,9 @@ using Xunit.Abstractions;
namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks
{
public class JpegBenchmarks : MeasureFixture
public class JpegProfilingBenchmarks : MeasureFixture
{
public JpegBenchmarks(ITestOutputHelper output)
public JpegProfilingBenchmarks(ITestOutputHelper output)
: base(output)
{
}

4
tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveBenchmarks.cs → tests/ImageSharp.Tests/ProfilingBenchmarks/LoadResizeSaveProfilingBenchmarks.cs

@ -10,9 +10,9 @@ using Xunit.Abstractions;
namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks
{
public class LoadResizeSaveBenchmarks : MeasureFixture
public class LoadResizeSaveProfilingBenchmarks : MeasureFixture
{
public LoadResizeSaveBenchmarks(ITestOutputHelper output)
public LoadResizeSaveProfilingBenchmarks(ITestOutputHelper output)
: base(output)
{
}

11
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs → tests/ImageSharp.Tests/ProfilingBenchmarks/ResizeProfilingBenchmarks.cs

@ -7,17 +7,10 @@ using SixLabors.ImageSharp.Processing;
using Xunit;
using Xunit.Abstractions;
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
namespace SixLabors.ImageSharp.Tests.ProfilingBenchmarks
{
public class ResizeProfilingBenchmarks : MeasureFixture
{
public const string SkipText =
#if false
null;
#else
"Benchmark, enable manually!";
#endif
private readonly Configuration configuration = Configuration.CreateDefaultInstance();
public ResizeProfilingBenchmarks(ITestOutputHelper output)
@ -28,7 +21,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
public int ExecutionCount { get; set; } = 50;
[Theory(Skip = SkipText)]
[Theory(Skip = ProfilingSetup.SkipProfilingTests)]
[InlineData(100, 100)]
[InlineData(2000, 2000)]
public void ResizeBicubic(int width, int height)

3
tests/ImageSharp.Tests/TestImages.cs

@ -46,6 +46,9 @@ namespace SixLabors.ImageSharp.Tests
public const string PDSrc = "Png/pd-source.png";
public const string PDDest = "Png/pd-dest.png";
public const string Gray1BitTrans = "Png/gray-1-trns.png";
public const string Gray2BitTrans = "Png/gray-2-tRNS.png";
public const string Gray4BitTrans = "Png/gray-4-tRNS.png";
public const string Gray8BitTrans = "Png/gray-8-tRNS.png";
// Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html
public const string Filter0 = "Png/filter0.png";

13
tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs

@ -11,7 +11,8 @@ namespace SixLabors.ImageSharp.Tests
/// </summary>
internal readonly struct ApproximateFloatComparer :
IEqualityComparer<float>,
IEqualityComparer<Vector4>
IEqualityComparer<Vector4>,
IEqualityComparer<Vector2>
{
private readonly float Epsilon;
@ -33,9 +34,17 @@ namespace SixLabors.ImageSharp.Tests
public int GetHashCode(float obj) => obj.GetHashCode();
/// <inheritdoc/>
public bool Equals(Vector4 x, Vector4 y) => this.Equals(x.X, y.X) && this.Equals(x.Y, y.Y) && this.Equals(x.Z, y.Z) && this.Equals(x.W, y.W);
public bool Equals(Vector4 a, Vector4 b) => this.Equals(a.X, b.X) && this.Equals(a.Y, b.Y) && this.Equals(a.Z, b.Z) && this.Equals(a.W, b.W);
/// <inheritdoc/>
public int GetHashCode(Vector4 obj) => obj.GetHashCode();
/// <inheritdoc/>
public bool Equals(Vector2 a, Vector2 b) => this.Equals(a.X, b.X) && this.Equals(a.Y, b.Y);
public int GetHashCode(Vector2 obj)
{
throw new System.NotImplementedException();
}
}
}

13
tests/ImageSharp.Tests/TestUtilities/TestUtils.cs

@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Primitives;
@ -284,5 +285,17 @@ namespace SixLabors.ImageSharp.Tests
}
public static string AsInvariantString(this FormattableString formattable) => System.FormattableString.Invariant(formattable);
public static IResampler GetResampler(string name)
{
PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name);
if (property is null)
{
throw new Exception($"No resampler named '{name}");
}
return (IResampler)property.GetValue(null);
}
}
}

2
tests/Images/External

@ -1 +1 @@
Subproject commit ed8a7b0b6fe1b2e2a7c822aa617103ae31192655
Subproject commit 5b18d8c95acffb773012881870ba6f521ba13128

BIN
tests/Images/Input/Png/gray-2-tRNS.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

BIN
tests/Images/Input/Png/gray-4-tRNS.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

BIN
tests/Images/Input/Png/gray-8-tRNS.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

Loading…
Cancel
Save