Browse Source

Merge branch 'master' into tiff-format

pull/1553/head
Brian Popow 5 years ago
committed by GitHub
parent
commit
387eeba77e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .gitattributes
  2. 2
      .github/PULL_REQUEST_TEMPLATE.md
  3. 2
      shared-infrastructure
  4. 32
      src/ImageSharp/Common/Helpers/RuntimeEnvironment.cs
  5. 59
      src/ImageSharp/Formats/Jpeg/Components/Encoder/LuminanceForwardConverter{TPixel}.cs
  6. 9
      src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs
  7. 21
      src/ImageSharp/Formats/Jpeg/JpegColorType.cs
  8. 1
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  9. 30
      src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
  10. 241
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  11. 15
      src/ImageSharp/Formats/Jpeg/JpegMetadata.cs
  12. 2
      src/ImageSharp/Formats/Jpeg/JpegSubsample.cs
  13. 210
      src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs
  14. 60
      src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs
  15. 104
      src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtils.cs
  16. 209
      src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs
  17. 4
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs
  18. 28
      tests/ImageSharp.Benchmarks/Processing/Rotate.cs
  19. 28
      tests/ImageSharp.Benchmarks/Processing/Skew.cs
  20. 31
      tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
  21. 4
      tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs
  22. 41
      tests/ImageSharp.Tests/Helpers/RuntimeEnvironmentTests.cs
  23. 5
      tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs
  24. 2
      tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs
  25. 94
      tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs
  26. 4
      tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs
  27. 7
      tests/ImageSharp.Tests/TestUtilities/TestEnvironment.cs
  28. 61
      tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs
  29. 3
      tests/ImageSharp.Tests/TestUtilities/Tests/TestEnvironmentTests.cs
  30. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(-20,-10).png
  31. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50__original.png
  32. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png
  33. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png
  34. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png
  35. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png
  36. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png
  37. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png
  38. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png
  39. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png
  40. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png
  41. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png
  42. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png
  43. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png
  44. 4
      tests/Images/External/ReferenceOutput/Drawing/DrawImageTests/DrawTransformed.png

3
.gitattributes

@ -86,7 +86,6 @@
*.dll binary
*.eot binary
*.exe binary
*.ktx binary
*.otf binary
*.pbm binary
*.pdf binary
@ -125,3 +124,5 @@
*.tga filter=lfs diff=lfs merge=lfs -text
*.webp filter=lfs diff=lfs merge=lfs -text
*.dds filter=lfs diff=lfs merge=lfs -text
*.ktx filter=lfs diff=lfs merge=lfs -text
*.ktx2 filter=lfs diff=lfs merge=lfs -text

2
.github/PULL_REQUEST_TEMPLATE.md

@ -2,7 +2,7 @@
- [ ] I have written a descriptive pull-request title
- [ ] I have verified that there are no overlapping [pull-requests](https://github.com/SixLabors/ImageSharp/pulls) open
- [ ] I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules :cop:.
- [ ] I have verified that I am following the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules :cop:.
- [ ] I have provided test coverage for my change (where applicable)
### Description

2
shared-infrastructure

@ -1 +1 @@
Subproject commit 06a733983486638b9e38197c7c6eb197ecac43e6
Subproject commit 9b1179f0ebe6a4dfed998252b860fa07fee54363

32
src/ImageSharp/Common/Helpers/RuntimeEnvironment.cs

@ -0,0 +1,32 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp
{
/// <summary>
/// Provides information about the .NET runtime installation.
/// Many methods defer to <see cref="RuntimeInformation"/> when available.
/// </summary>
internal static class RuntimeEnvironment
{
private static readonly Lazy<bool> IsNetCoreLazy = new Lazy<bool>(() => FrameworkDescription.StartsWith(".NET Core", StringComparison.OrdinalIgnoreCase));
/// <summary>
/// Gets a value indicating whether the .NET installation is .NET Core 3.1 or lower.
/// </summary>
public static bool IsNetCore => IsNetCoreLazy.Value;
/// <summary>
/// Gets the name of the .NET installation on which an app is running.
/// </summary>
public static string FrameworkDescription => RuntimeInformation.FrameworkDescription;
/// <summary>
/// Indicates whether the current application is running on the specified platform.
/// </summary>
public static bool IsOSPlatform(OSPlatform osPlatform) => RuntimeInformation.IsOSPlatform(osPlatform);
}
}

59
src/ImageSharp/Formats/Jpeg/Components/Encoder/LuminanceForwardConverter{TPixel}.cs

@ -0,0 +1,59 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder
{
/// <summary>
/// On-stack worker struct to efficiently encapsulate the TPixel -> L8 -> Y conversion chain of 8x8 pixel blocks.
/// </summary>
/// <typeparam name="TPixel">The pixel type to work on</typeparam>
internal ref struct LuminanceForwardConverter<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// The Y component
/// </summary>
public Block8x8F Y;
/// <summary>
/// Temporal 8x8 block to hold TPixel data
/// </summary>
private GenericBlock8x8<TPixel> pixelBlock;
/// <summary>
/// Temporal RGB block
/// </summary>
private GenericBlock8x8<L8> l8Block;
public static LuminanceForwardConverter<TPixel> Create()
{
var result = default(LuminanceForwardConverter<TPixel>);
return result;
}
/// <summary>
/// Converts a 8x8 image area inside 'pixels' at position (x,y) placing the result members of the structure (<see cref="Y"/>)
/// </summary>
public void Convert(ImageFrame<TPixel> frame, int x, int y, ref RowOctet<TPixel> currentRows)
{
this.pixelBlock.LoadAndStretchEdges(frame.PixelBuffer, x, y, ref currentRows);
Span<L8> l8Span = this.l8Block.AsSpanUnsafe();
PixelOperations<TPixel>.Instance.ToL8(frame.GetConfiguration(), this.pixelBlock.AsSpanUnsafe(), l8Span);
ref Block8x8F yBlock = ref this.Y;
ref L8 l8Start = ref l8Span[0];
for (int i = 0; i < 64; i++)
{
ref L8 c = ref Unsafe.Add(ref l8Start, i);
yBlock[i] = c.PackedValue;
}
}
}
}

9
src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Jpeg
@ -20,5 +20,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
/// <value>The subsample ratio of the jpg image.</value>
JpegSubsample? Subsample { get; }
/// <summary>
/// Gets the color type.
/// </summary>
JpegColorType? ColorType { get; }
}
}
}

21
src/ImageSharp/Formats/Jpeg/JpegColorType.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Jpeg
{
/// <summary>
/// Provides enumeration of available JPEG color types.
/// </summary>
public enum JpegColorType : byte
{
/// <summary>
/// YCbCr (luminance, blue chroma, red chroma) color as defined in the ITU-T T.871 specification.
/// </summary>
YCbCr = 0,
/// <summary>
/// Single channel, luminance.
/// </summary>
Luminance = 1
}
}

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

@ -912,6 +912,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.Frame.MaxHorizontalFactor = maxH;
this.Frame.MaxVerticalFactor = maxV;
this.ColorSpace = this.DeduceJpegColorSpace();
this.Metadata.GetJpegMetadata().ColorType = this.ColorSpace == JpegColorSpace.Grayscale ? JpegColorType.Luminance : JpegColorType.YCbCr;
this.Frame.InitComponents();
this.ImageSizeInMCU = new Size(this.Frame.McusPerLine, this.Frame.McusPerColumn);
}

30
src/ImageSharp/Formats/Jpeg/JpegEncoder.cs

@ -25,6 +25,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
public JpegSubsample? Subsample { get; set; }
/// <summary>
/// Gets or sets the color type, that will be used to encode the image.
/// </summary>
public JpegColorType? ColorType { get; set; }
/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// </summary>
@ -35,6 +40,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
where TPixel : unmanaged, IPixel<TPixel>
{
var encoder = new JpegEncoderCore(this);
this.InitializeColorType<TPixel>(image);
encoder.Encode(image, stream);
}
@ -50,7 +56,31 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
where TPixel : unmanaged, IPixel<TPixel>
{
var encoder = new JpegEncoderCore(this);
this.InitializeColorType<TPixel>(image);
return encoder.EncodeAsync(image, stream, cancellationToken);
}
/// <summary>
/// If ColorType was not set, set it based on the given image.
/// </summary>
private void InitializeColorType<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
// First inspect the image metadata.
if (this.ColorType == null)
{
JpegMetadata metadata = image.Metadata.GetJpegMetadata();
this.ColorType = metadata.ColorType;
}
// Secondly, inspect the pixel type.
if (this.ColorType == null)
{
bool isGrayscale =
typeof(TPixel) == typeof(L8) || typeof(TPixel) == typeof(L16) ||
typeof(TPixel) == typeof(La16) || typeof(TPixel) == typeof(La32);
this.ColorType = isGrayscale ? JpegColorType.Luminance : JpegColorType.YCbCr;
}
}
}
}

241
src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs

@ -57,6 +57,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
private readonly int? quality;
/// <summary>
/// Gets or sets the subsampling method to use.
/// </summary>
private readonly JpegColorType? colorType;
/// <summary>
/// The accumulated bits to write to the stream.
/// </summary>
@ -90,6 +95,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
{
this.quality = options.Quality;
this.subsample = options.Subsample;
this.colorType = options.ColorType;
}
/// <summary>
@ -115,42 +121,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
8, 8, 8,
};
/// <summary>
/// Gets the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes:
/// - the marker length "\x00\x0c",
/// - the number of components "\x03",
/// - component 1 uses DC table 0 and AC table 0 "\x01\x00",
/// - component 2 uses DC table 1 and AC table 1 "\x02\x11",
/// - component 3 uses DC table 1 and AC table 1 "\x03\x11",
/// - the bytes "\x00\x3f\x00". Section B.2.3 of the spec says that for
/// sequential DCTs, those bytes (8-bit Ss, 8-bit Se, 4-bit Ah, 4-bit Al)
/// should be 0x00, 0x3f, 0x00&lt;&lt;4 | 0x00.
/// </summary>
// The C# compiler emits this as a compile-time constant embedded in the PE file.
// This is effectively compiled down to: return new ReadOnlySpan<byte>(&data, length)
// More details can be found: https://github.com/dotnet/roslyn/pull/24621
private static ReadOnlySpan<byte> SosHeaderYCbCr => new byte[]
{
JpegConstants.Markers.XFF, JpegConstants.Markers.SOS,
// Marker
0x00, 0x0c,
// Length (high byte, low byte), must be 6 + 2 * (number of components in scan)
0x03, // Number of components in a scan, 3
0x01, // Component Id Y
0x00, // DC/AC Huffman table
0x02, // Component Id Cb
0x11, // DC/AC Huffman table
0x03, // Component Id Cr
0x11, // DC/AC Huffman table
0x00, // Ss - Start of spectral selection.
0x3f, // Se - End of spectral selection.
0x00
// Ah + Ah (Successive approximation bit position high + low)
};
/// <summary>
/// Gets the unscaled quantization tables in zig-zag order. Each
/// encoder copies and scales the tables according to its quality parameter.
@ -212,6 +182,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.outputStream = stream;
ImageMetadata metadata = image.Metadata;
// Compute number of components based on color type in options.
int componentCount = (this.colorType == JpegColorType.Luminance) ? 1 : 3;
// System.Drawing produces identical output for jpegs with a quality parameter of 0 and 1.
int qlty = Numerics.Clamp(this.quality ?? metadata.GetJpegMetadata().Quality, 1, 100);
this.subsample ??= qlty >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420;
@ -229,10 +202,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// Initialize the quantization tables.
InitQuantizationTable(0, scale, ref this.luminanceQuantTable);
InitQuantizationTable(1, scale, ref this.chrominanceQuantTable);
// Compute number of components based on input image type.
const int componentCount = 3;
if (componentCount > 1)
{
InitQuantizationTable(1, scale, ref this.chrominanceQuantTable);
}
// Write the Start Of Image marker.
this.WriteApplicationHeader(metadata);
@ -250,7 +223,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.WriteDefineHuffmanTables(componentCount);
// Write the image data.
this.WriteStartOfScan(image, cancellationToken);
this.WriteStartOfScan(image, componentCount, cancellationToken);
// Write the End Of Image marker.
this.buffer[0] = JpegConstants.Markers.XFF;
@ -468,6 +441,55 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
}
/// <summary>
/// Encodes the image with no chroma, just luminance.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The pixel accessor providing access to the image pixels.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param>
/// <param name="emitBufferBase">The reference to the emit buffer.</param>
private void EncodeGrayscale<TPixel>(Image<TPixel> pixels, CancellationToken cancellationToken, ref byte emitBufferBase)
where TPixel : unmanaged, IPixel<TPixel>
{
// TODO: Need a JpegScanEncoder<TPixel> class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.)
// (Partially done with YCbCrForwardConverter<TPixel>)
Block8x8F temp1 = default;
Block8x8F temp2 = default;
Block8x8F onStackLuminanceQuantTable = this.luminanceQuantTable;
var unzig = ZigZag.CreateUnzigTable();
// ReSharper disable once InconsistentNaming
int prevDCY = 0;
var pixelConverter = LuminanceForwardConverter<TPixel>.Create();
ImageFrame<TPixel> frame = pixels.Frames.RootFrame;
Buffer2D<TPixel> pixelBuffer = frame.PixelBuffer;
RowOctet<TPixel> currentRows = default;
for (int y = 0; y < pixels.Height; y += 8)
{
cancellationToken.ThrowIfCancellationRequested();
currentRows.Update(pixelBuffer, y);
for (int x = 0; x < pixels.Width; x += 8)
{
pixelConverter.Convert(frame, x, y, ref currentRows);
prevDCY = this.WriteBlock(
QuantIndex.Luminance,
prevDCY,
ref pixelConverter.Y,
ref temp1,
ref temp2,
ref onStackLuminanceQuantTable,
ref unzig,
ref emitBufferBase);
}
}
}
/// <summary>
/// Writes the application header containing the JFIF identifier plus extra data.
/// </summary>
@ -896,24 +918,36 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
0x01
};
switch (this.subsample)
if (this.colorType == JpegColorType.Luminance)
{
case JpegSubsample.Ratio444:
subsamples = stackalloc byte[]
{
0x11,
0x11,
0x11
};
break;
case JpegSubsample.Ratio420:
subsamples = stackalloc byte[]
{
0x22,
0x11,
0x11
};
break;
subsamples = stackalloc byte[]
{
0x11,
0x00,
0x00
};
}
else
{
switch (this.subsample)
{
case JpegSubsample.Ratio444:
subsamples = stackalloc byte[]
{
0x11,
0x11,
0x11
};
break;
case JpegSubsample.Ratio420:
subsamples = stackalloc byte[]
{
0x22,
0x11,
0x11
};
break;
}
}
// Length (high byte, low byte), 8 + components * 3.
@ -926,26 +960,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.buffer[4] = (byte)(width & 0xff); // (2 bytes, Hi-Lo), must be > 0 if DNL not supported
this.buffer[5] = (byte)componentCount;
// Number of components (1 byte), usually 1 = Gray scaled, 3 = color YCbCr or YIQ, 4 = color CMYK)
if (componentCount == 1)
for (int i = 0; i < componentCount; i++)
{
this.buffer[6] = 1;
int i3 = 3 * i;
this.buffer[i3 + 6] = (byte)(i + 1);
// No subsampling for grayscale images.
this.buffer[7] = 0x11;
this.buffer[8] = 0x00;
}
else
{
for (int i = 0; i < componentCount; i++)
{
int i3 = 3 * i;
this.buffer[i3 + 6] = (byte)(i + 1);
// We use 4:2:0 chroma subsampling by default.
this.buffer[i3 + 7] = subsamples[i];
this.buffer[i3 + 8] = chroma[i];
}
this.buffer[i3 + 7] = subsamples[i];
this.buffer[i3 + 8] = chroma[i];
}
this.outputStream.Write(this.buffer, 0, (3 * (componentCount - 1)) + 9);
@ -956,22 +977,70 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The pixel accessor providing access to the image pixels.</param>
/// <param name="componentCount">The number of components in a pixel.</param>
/// <param name="cancellationToken">The token to monitor for cancellation.</param>
private void WriteStartOfScan<TPixel>(Image<TPixel> image, CancellationToken cancellationToken)
private void WriteStartOfScan<TPixel>(Image<TPixel> image, int componentCount, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
// TODO: Need a JpegScanEncoder<TPixel> class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.)
// TODO: We should allow grayscale writing.
this.outputStream.Write(SosHeaderYCbCr);
Span<byte> componentId = stackalloc byte[]
{
0x01,
0x02,
0x03
};
Span<byte> huffmanId = stackalloc byte[]
{
0x00,
0x11,
0x11
};
// Write the SOS (Start Of Scan) marker "\xff\xda" followed by 12 bytes:
// - the marker length "\x00\x0c",
// - the number of components "\x03",
// - component 1 uses DC table 0 and AC table 0 "\x01\x00",
// - component 2 uses DC table 1 and AC table 1 "\x02\x11",
// - component 3 uses DC table 1 and AC table 1 "\x03\x11",
// - the bytes "\x00\x3f\x00". Section B.2.3 of the spec says that for
// sequential DCTs, those bytes (8-bit Ss, 8-bit Se, 4-bit Ah, 4-bit Al)
// should be 0x00, 0x3f, 0x00&lt;&lt;4 | 0x00.
this.buffer[0] = JpegConstants.Markers.XFF;
this.buffer[1] = JpegConstants.Markers.SOS;
// Length (high byte, low byte), must be 6 + 2 * (number of components in scan)
int sosSize = 6 + (2 * componentCount);
this.buffer[2] = 0x00;
this.buffer[3] = (byte)sosSize;
this.buffer[4] = (byte)componentCount; // Number of components in a scan
for (int i = 0; i < componentCount; i++)
{
int i2 = 2 * i;
this.buffer[i2 + 5] = componentId[i]; // Component Id
this.buffer[i2 + 6] = huffmanId[i]; // DC/AC Huffman table
}
this.buffer[sosSize - 1] = 0x00; // Ss - Start of spectral selection.
this.buffer[sosSize] = 0x3f; // Se - End of spectral selection.
this.buffer[sosSize + 1] = 0x00; // Ah + Ah (Successive approximation bit position high + low)
this.outputStream.Write(this.buffer, 0, sosSize + 2);
ref byte emitBufferBase = ref MemoryMarshal.GetReference<byte>(this.emitBuffer);
switch (this.subsample)
if (this.colorType == JpegColorType.Luminance)
{
case JpegSubsample.Ratio444:
this.Encode444(image, cancellationToken, ref emitBufferBase);
break;
case JpegSubsample.Ratio420:
this.Encode420(image, cancellationToken, ref emitBufferBase);
break;
this.EncodeGrayscale(image, cancellationToken, ref emitBufferBase);
}
else
{
switch (this.subsample)
{
case JpegSubsample.Ratio444:
this.Encode444(image, cancellationToken, ref emitBufferBase);
break;
case JpegSubsample.Ratio420:
this.Encode420(image, cancellationToken, ref emitBufferBase);
break;
}
}
// Pad the last byte with 1's.

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

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Jpeg
@ -19,14 +19,23 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// Initializes a new instance of the <see cref="JpegMetadata"/> class.
/// </summary>
/// <param name="other">The metadata to create an instance from.</param>
private JpegMetadata(JpegMetadata other) => this.Quality = other.Quality;
private JpegMetadata(JpegMetadata other)
{
this.Quality = other.Quality;
this.ColorType = other.ColorType;
}
/// <summary>
/// Gets or sets the encoded quality.
/// </summary>
public int Quality { get; set; } = 75;
/// <summary>
/// Gets or sets the encoded quality.
/// </summary>
public JpegColorType? ColorType { get; set; }
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new JpegMetadata(this);
}
}
}

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

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Jpeg

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

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -80,32 +82,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return;
}
int yRadius = LinearTransformUtils.GetSamplingRadius(in sampler, source.Height, destination.Height);
int xRadius = LinearTransformUtils.GetSamplingRadius(in sampler, source.Width, destination.Width);
var radialExtents = new Vector2(xRadius, yRadius);
int yLength = (yRadius * 2) + 1;
int xLength = (xRadius * 2) + 1;
// We use 2D buffers so that we can access the weight spans per row in parallel.
using Buffer2D<float> yKernelBuffer = configuration.MemoryAllocator.Allocate2D<float>(yLength, destination.Height);
using Buffer2D<float> xKernelBuffer = configuration.MemoryAllocator.Allocate2D<float>(xLength, destination.Height);
int maxX = source.Width - 1;
int maxY = source.Height - 1;
var maxSourceExtents = new Vector4(maxX, maxY, maxX, maxY);
var operation = new AffineOperation<TResampler>(
configuration,
source,
destination,
yKernelBuffer,
xKernelBuffer,
in sampler,
matrix,
radialExtents,
maxSourceExtents);
matrix);
ParallelRowIterator.IterateRows<AffineOperation<TResampler>, Vector4>(
ParallelRowIterator.IterateRowIntervals<AffineOperation<TResampler>, Vector4>(
configuration,
destination.Bounds(),
in operation);
@ -117,7 +101,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
private readonly ImageFrame<TPixel> destination;
private readonly Rectangle bounds;
private readonly Matrix3x2 matrix;
private readonly int maxX;
[MethodImpl(InliningOptions.ShortMethod)]
public NNAffineOperation(
@ -129,15 +112,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
this.destination = destination;
this.bounds = source.Bounds();
this.matrix = matrix;
this.maxX = destination.Width;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y)
{
Buffer2D<TPixel> sourceBuffer = this.source.PixelBuffer;
Span<TPixel> destRow = this.destination.GetPixelRowSpan(y);
for (int x = 0; x < this.maxX; x++)
for (int x = 0; x < destRow.Length; x++)
{
var point = Vector2.Transform(new Vector2(x, y), this.matrix);
int px = (int)MathF.Round(point.X);
@ -145,84 +128,181 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
if (this.bounds.Contains(px, py))
{
destRow[x] = this.source[px, py];
destRow[x] = sourceBuffer.GetElementUnsafe(px, py);
}
}
}
}
private readonly struct AffineOperation<TResampler> : IRowOperation<Vector4>
private readonly struct AffineOperation<TResampler> : IRowIntervalOperation<Vector4>
where TResampler : struct, IResampler
{
private readonly Configuration configuration;
private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination;
private readonly Buffer2D<float> yKernelBuffer;
private readonly Buffer2D<float> xKernelBuffer;
private readonly TResampler sampler;
private readonly Matrix3x2 matrix;
private readonly Vector2 radialExtents;
private readonly Vector4 maxSourceExtents;
private readonly int maxX;
private readonly float yRadius;
private readonly float xRadius;
[MethodImpl(InliningOptions.ShortMethod)]
public AffineOperation(
Configuration configuration,
ImageFrame<TPixel> source,
ImageFrame<TPixel> destination,
Buffer2D<float> yKernelBuffer,
Buffer2D<float> xKernelBuffer,
in TResampler sampler,
Matrix3x2 matrix,
Vector2 radialExtents,
Vector4 maxSourceExtents)
Matrix3x2 matrix)
{
this.configuration = configuration;
this.source = source;
this.destination = destination;
this.yKernelBuffer = yKernelBuffer;
this.xKernelBuffer = xKernelBuffer;
this.sampler = sampler;
this.matrix = matrix;
this.radialExtents = radialExtents;
this.maxSourceExtents = maxSourceExtents;
this.maxX = destination.Width;
this.yRadius = LinearTransformUtility.GetSamplingRadius(in sampler, source.Height, destination.Height);
this.xRadius = LinearTransformUtility.GetSamplingRadius(in sampler, source.Width, destination.Width);
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<Vector4> span)
public void Invoke(in RowInterval rows, Span<Vector4> span)
{
Buffer2D<TPixel> sourceBuffer = this.source.PixelBuffer;
if (RuntimeEnvironment.IsOSPlatform(OSPlatform.OSX)
&& RuntimeEnvironment.IsNetCore)
{
// There's something wrong with the JIT in .NET Core 3.1 on certain
// MacOSX machines so we have to use different pipelines.
// It's:
// - Not reproducable locally
// - Doesn't seem to be triggered by the bulk Numerics.UnPremultiply method but by caller.
// https://github.com/SixLabors/ImageSharp/pull/1591
this.InvokeMacOSX(in rows, span);
return;
}
PixelOperations<TPixel>.Instance.ToVector4(
this.configuration,
this.destination.GetPixelRowSpan(y),
span);
Matrix3x2 matrix = this.matrix;
TResampler sampler = this.sampler;
float yRadius = this.yRadius;
float xRadius = this.xRadius;
int maxY = this.source.Height - 1;
int maxX = this.source.Width - 1;
ref float yKernelSpanRef = ref MemoryMarshal.GetReference(this.yKernelBuffer.GetRowSpan(y));
ref float xKernelSpanRef = ref MemoryMarshal.GetReference(this.xKernelBuffer.GetRowSpan(y));
Buffer2D<TPixel> sourceBuffer = this.source.PixelBuffer;
for (int x = 0; x < this.maxX; x++)
for (int y = rows.Min; y < rows.Max; y++)
{
// 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), this.matrix);
LinearTransformUtils.Convolve(
in this.sampler,
point,
sourceBuffer,
Span<TPixel> rowSpan = this.destination.GetPixelRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(
this.configuration,
rowSpan,
span,
PixelConversionModifiers.Scale);
for (int x = 0; x < span.Length; x++)
{
var point = Vector2.Transform(new Vector2(x, y), matrix);
float pY = point.Y;
float pX = point.X;
int top = LinearTransformUtility.GetRangeStart(yRadius, pY, maxY);
int bottom = LinearTransformUtility.GetRangeEnd(yRadius, pY, maxY);
int left = LinearTransformUtility.GetRangeStart(xRadius, pX, maxX);
int right = LinearTransformUtility.GetRangeEnd(xRadius, pX, maxX);
if (bottom == top || right == left)
{
continue;
}
Vector4 sum = Vector4.Zero;
for (int yK = top; yK <= bottom; yK++)
{
float yWeight = sampler.GetValue(yK - pY);
for (int xK = left; xK <= right; xK++)
{
float xWeight = sampler.GetValue(xK - pX);
Vector4 current = sourceBuffer.GetElementUnsafe(xK, yK).ToScaledVector4();
Numerics.Premultiply(ref current);
sum += current * xWeight * yWeight;
}
}
span[x] = sum;
}
Numerics.UnPremultiply(span);
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
span,
x,
ref yKernelSpanRef,
ref xKernelSpanRef,
this.radialExtents,
this.maxSourceExtents);
rowSpan,
PixelConversionModifiers.Scale);
}
}
[ExcludeFromCodeCoverage]
[MethodImpl(InliningOptions.ShortMethod)]
private void InvokeMacOSX(in RowInterval rows, Span<Vector4> span)
{
Matrix3x2 matrix = this.matrix;
TResampler sampler = this.sampler;
float yRadius = this.yRadius;
float xRadius = this.xRadius;
int maxY = this.source.Height - 1;
int maxX = this.source.Width - 1;
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
span,
this.destination.GetPixelRowSpan(y));
Buffer2D<TPixel> sourceBuffer = this.source.PixelBuffer;
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> rowSpan = this.destination.GetPixelRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(
this.configuration,
rowSpan,
span,
PixelConversionModifiers.Scale);
for (int x = 0; x < span.Length; x++)
{
var point = Vector2.Transform(new Vector2(x, y), matrix);
float pY = point.Y;
float pX = point.X;
int top = LinearTransformUtility.GetRangeStart(yRadius, pY, maxY);
int bottom = LinearTransformUtility.GetRangeEnd(yRadius, pY, maxY);
int left = LinearTransformUtility.GetRangeStart(xRadius, pX, maxX);
int right = LinearTransformUtility.GetRangeEnd(xRadius, pX, maxX);
if (bottom == top || right == left)
{
continue;
}
Vector4 sum = Vector4.Zero;
for (int yK = top; yK <= bottom; yK++)
{
float yWeight = sampler.GetValue(yK - pY);
for (int xK = left; xK <= right; xK++)
{
float xWeight = sampler.GetValue(xK - pX);
Vector4 current = sourceBuffer.GetElementUnsafe(xK, yK).ToScaledVector4();
Numerics.Premultiply(ref current);
sum += current * xWeight * yWeight;
}
}
Numerics.UnPremultiply(ref sum);
span[x] = sum;
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
span,
rowSpan,
PixelConversionModifiers.Scale);
}
}
}
}

60
src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs

@ -0,0 +1,60 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Utility methods for linear transforms.
/// </summary>
internal static class LinearTransformUtility
{
/// <summary>
/// Returns the sampling radius for the given sampler and dimensions.
/// </summary>
/// <typeparam name="TResampler">The type of resampler.</typeparam>
/// <param name="sampler">The resampler sampler.</param>
/// <param name="sourceSize">The source size.</param>
/// <param name="destinationSize">The destination size.</param>
/// <returns>The <see cref="float"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static float GetSamplingRadius<TResampler>(in TResampler sampler, int sourceSize, int destinationSize)
where TResampler : struct, IResampler
{
float scale = (float)sourceSize / destinationSize;
if (scale < 1F)
{
scale = 1F;
}
return MathF.Ceiling(sampler.Radius * scale);
}
/// <summary>
/// Gets the start position (inclusive) for a sampling range given
/// the radius, center position and max constraint.
/// </summary>
/// <param name="radius">The radius.</param>
/// <param name="center">The center position.</param>
/// <param name="max">The max allowed amouunt.</param>
/// <returns>The <see cref="int"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static int GetRangeStart(float radius, float center, int max)
=> Numerics.Clamp((int)MathF.Ceiling(center - radius), 0, max);
/// <summary>
/// Gets the end position (inclusive) for a sampling range given
/// the radius, center position and max constraint.
/// </summary>
/// <param name="radius">The radius.</param>
/// <param name="center">The center position.</param>
/// <param name="max">The max allowed amouunt.</param>
/// <returns>The <see cref="int"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static int GetRangeEnd(float radius, float center, int max)
=> Numerics.Clamp((int)MathF.Floor(center + radius), 0, max);
}
}

104
src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtils.cs

@ -1,104 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Utility methods for affine and projective transforms.
/// </summary>
internal static class LinearTransformUtils
{
[MethodImpl(InliningOptions.ShortMethod)]
internal static int GetSamplingRadius<TResampler>(in TResampler sampler, int sourceSize, int destinationSize)
where TResampler : struct, IResampler
{
double scale = sourceSize / destinationSize;
if (scale < 1)
{
scale = 1;
}
return (int)Math.Ceiling(scale * sampler.Radius);
}
[MethodImpl(InliningOptions.ShortMethod)]
internal static void Convolve<TResampler, TPixel>(
in TResampler sampler,
Vector2 transformedPoint,
Buffer2D<TPixel> sourcePixels,
Span<Vector4> targetRow,
int column,
ref float yKernelSpanRef,
ref float xKernelSpanRef,
Vector2 radialExtents,
Vector4 maxSourceExtents)
where TResampler : struct, IResampler
where TPixel : unmanaged, IPixel<TPixel>
{
// Clamp sampling pixel radial extents to the source image edges
Vector2 minXY = transformedPoint - radialExtents;
Vector2 maxXY = transformedPoint + radialExtents;
// left, top, right, bottom
var sourceExtents = new Vector4(
MathF.Ceiling(minXY.X),
MathF.Ceiling(minXY.Y),
MathF.Floor(maxXY.X),
MathF.Floor(maxXY.Y));
sourceExtents = Numerics.Clamp(sourceExtents, Vector4.Zero, maxSourceExtents);
int left = (int)sourceExtents.X;
int top = (int)sourceExtents.Y;
int right = (int)sourceExtents.Z;
int bottom = (int)sourceExtents.W;
if (left == right || top == bottom)
{
return;
}
CalculateWeights(in sampler, top, bottom, transformedPoint.Y, ref yKernelSpanRef);
CalculateWeights(in sampler, left, right, transformedPoint.X, ref xKernelSpanRef);
Vector4 sum = Vector4.Zero;
for (int kernelY = 0, y = top; y <= bottom; y++, kernelY++)
{
float yWeight = Unsafe.Add(ref yKernelSpanRef, kernelY);
for (int kernelX = 0, x = left; x <= right; x++, kernelX++)
{
float xWeight = Unsafe.Add(ref xKernelSpanRef, kernelX);
// Values are first premultiplied to prevent darkening of edge pixels.
var current = sourcePixels[x, y].ToVector4();
Numerics.Premultiply(ref current);
sum += current * xWeight * yWeight;
}
}
// Reverse the premultiplication
Numerics.UnPremultiply(ref sum);
targetRow[column] = sum;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static void CalculateWeights<TResampler>(in TResampler sampler, int min, int max, float point, ref float weightsRef)
where TResampler : struct, IResampler
{
float sum = 0;
for (int x = 0, i = min; i <= max; i++, x++)
{
float weight = sampler.GetValue(i - point);
sum += weight;
Unsafe.Add(ref weightsRef, x) = weight;
}
}
}
}

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

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -80,32 +81,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return;
}
int yRadius = LinearTransformUtils.GetSamplingRadius(in sampler, source.Height, destination.Height);
int xRadius = LinearTransformUtils.GetSamplingRadius(in sampler, source.Width, destination.Width);
var radialExtents = new Vector2(xRadius, yRadius);
int yLength = (yRadius * 2) + 1;
int xLength = (xRadius * 2) + 1;
// We use 2D buffers so that we can access the weight spans per row in parallel.
using Buffer2D<float> yKernelBuffer = configuration.MemoryAllocator.Allocate2D<float>(yLength, destination.Height);
using Buffer2D<float> xKernelBuffer = configuration.MemoryAllocator.Allocate2D<float>(xLength, destination.Height);
int maxX = source.Width - 1;
int maxY = source.Height - 1;
var maxSourceExtents = new Vector4(maxX, maxY, maxX, maxY);
var operation = new ProjectiveOperation<TResampler>(
configuration,
source,
destination,
yKernelBuffer,
xKernelBuffer,
in sampler,
matrix,
radialExtents,
maxSourceExtents);
matrix);
ParallelRowIterator.IterateRows<ProjectiveOperation<TResampler>, Vector4>(
ParallelRowIterator.IterateRowIntervals<ProjectiveOperation<TResampler>, Vector4>(
configuration,
destination.Bounds(),
in operation);
@ -117,7 +100,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
private readonly ImageFrame<TPixel> destination;
private readonly Rectangle bounds;
private readonly Matrix4x4 matrix;
private readonly int maxX;
[MethodImpl(InliningOptions.ShortMethod)]
public NNProjectiveOperation(
@ -129,15 +111,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
this.destination = destination;
this.bounds = source.Bounds();
this.matrix = matrix;
this.maxX = destination.Width;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y)
{
Buffer2D<TPixel> sourceBuffer = this.source.PixelBuffer;
Span<TPixel> destRow = this.destination.GetPixelRowSpan(y);
for (int x = 0; x < this.maxX; x++)
for (int x = 0; x < destRow.Length; x++)
{
Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, this.matrix);
int px = (int)MathF.Round(point.X);
@ -145,84 +127,181 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
if (this.bounds.Contains(px, py))
{
destRow[x] = this.source[px, py];
destRow[x] = sourceBuffer.GetElementUnsafe(px, py);
}
}
}
}
private readonly struct ProjectiveOperation<TResampler> : IRowOperation<Vector4>
private readonly struct ProjectiveOperation<TResampler> : IRowIntervalOperation<Vector4>
where TResampler : struct, IResampler
{
private readonly Configuration configuration;
private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination;
private readonly Buffer2D<float> yKernelBuffer;
private readonly Buffer2D<float> xKernelBuffer;
private readonly TResampler sampler;
private readonly Matrix4x4 matrix;
private readonly Vector2 radialExtents;
private readonly Vector4 maxSourceExtents;
private readonly int maxX;
private readonly float yRadius;
private readonly float xRadius;
[MethodImpl(InliningOptions.ShortMethod)]
public ProjectiveOperation(
Configuration configuration,
ImageFrame<TPixel> source,
ImageFrame<TPixel> destination,
Buffer2D<float> yKernelBuffer,
Buffer2D<float> xKernelBuffer,
in TResampler sampler,
Matrix4x4 matrix,
Vector2 radialExtents,
Vector4 maxSourceExtents)
Matrix4x4 matrix)
{
this.configuration = configuration;
this.source = source;
this.destination = destination;
this.yKernelBuffer = yKernelBuffer;
this.xKernelBuffer = xKernelBuffer;
this.sampler = sampler;
this.matrix = matrix;
this.radialExtents = radialExtents;
this.maxSourceExtents = maxSourceExtents;
this.maxX = destination.Width;
this.yRadius = LinearTransformUtility.GetSamplingRadius(in sampler, source.Height, destination.Height);
this.xRadius = LinearTransformUtility.GetSamplingRadius(in sampler, source.Width, destination.Width);
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y, Span<Vector4> span)
public void Invoke(in RowInterval rows, Span<Vector4> span)
{
Buffer2D<TPixel> sourceBuffer = this.source.PixelBuffer;
if (RuntimeEnvironment.IsOSPlatform(OSPlatform.OSX)
&& RuntimeEnvironment.IsNetCore)
{
// There's something wrong with the JIT in .NET Core 3.1 on certain
// MacOSX machines so we have to use different pipelines.
// It's:
// - Not reproducable locally
// - Doesn't seem to be triggered by the bulk Numerics.UnPremultiply method but by caller.
// https://github.com/SixLabors/ImageSharp/pull/1591
this.InvokeMacOSX(in rows, span);
return;
}
PixelOperations<TPixel>.Instance.ToVector4(
this.configuration,
this.destination.GetPixelRowSpan(y),
span);
Matrix4x4 matrix = this.matrix;
TResampler sampler = this.sampler;
float yRadius = this.yRadius;
float xRadius = this.xRadius;
int maxY = this.source.Height - 1;
int maxX = this.source.Width - 1;
ref float yKernelSpanRef = ref MemoryMarshal.GetReference(this.yKernelBuffer.GetRowSpan(y));
ref float xKernelSpanRef = ref MemoryMarshal.GetReference(this.xKernelBuffer.GetRowSpan(y));
Buffer2D<TPixel> sourceBuffer = this.source.PixelBuffer;
for (int x = 0; x < this.maxX; x++)
for (int y = rows.Min; y < rows.Max; y++)
{
// Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds.
Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, this.matrix);
LinearTransformUtils.Convolve(
in this.sampler,
point,
sourceBuffer,
Span<TPixel> rowSpan = this.destination.GetPixelRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(
this.configuration,
rowSpan,
span,
PixelConversionModifiers.Scale);
for (int x = 0; x < span.Length; x++)
{
Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, matrix);
float pY = point.Y;
float pX = point.X;
int top = LinearTransformUtility.GetRangeStart(yRadius, pY, maxY);
int bottom = LinearTransformUtility.GetRangeEnd(yRadius, pY, maxY);
int left = LinearTransformUtility.GetRangeStart(xRadius, pX, maxX);
int right = LinearTransformUtility.GetRangeEnd(xRadius, pX, maxX);
if (bottom <= top || right <= left)
{
continue;
}
Vector4 sum = Vector4.Zero;
for (int yK = top; yK <= bottom; yK++)
{
float yWeight = sampler.GetValue(yK - pY);
for (int xK = left; xK <= right; xK++)
{
float xWeight = sampler.GetValue(xK - pX);
Vector4 current = sourceBuffer.GetElementUnsafe(xK, yK).ToScaledVector4();
Numerics.Premultiply(ref current);
sum += current * xWeight * yWeight;
}
}
span[x] = sum;
}
Numerics.UnPremultiply(span);
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
span,
x,
ref yKernelSpanRef,
ref xKernelSpanRef,
this.radialExtents,
this.maxSourceExtents);
rowSpan,
PixelConversionModifiers.Scale);
}
}
[ExcludeFromCodeCoverage]
[MethodImpl(InliningOptions.ShortMethod)]
public void InvokeMacOSX(in RowInterval rows, Span<Vector4> span)
{
Matrix4x4 matrix = this.matrix;
TResampler sampler = this.sampler;
float yRadius = this.yRadius;
float xRadius = this.xRadius;
int maxY = this.source.Height - 1;
int maxX = this.source.Width - 1;
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
span,
this.destination.GetPixelRowSpan(y));
Buffer2D<TPixel> sourceBuffer = this.source.PixelBuffer;
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> rowSpan = this.destination.GetPixelRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(
this.configuration,
rowSpan,
span,
PixelConversionModifiers.Scale);
for (int x = 0; x < span.Length; x++)
{
Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, matrix);
float pY = point.Y;
float pX = point.X;
int top = LinearTransformUtility.GetRangeStart(yRadius, pY, maxY);
int bottom = LinearTransformUtility.GetRangeEnd(yRadius, pY, maxY);
int left = LinearTransformUtility.GetRangeStart(xRadius, pX, maxX);
int right = LinearTransformUtility.GetRangeEnd(xRadius, pX, maxX);
if (bottom <= top || right <= left)
{
continue;
}
Vector4 sum = Vector4.Zero;
for (int yK = top; yK <= bottom; yK++)
{
float yWeight = sampler.GetValue(yK - pY);
for (int xK = left; xK <= right; xK++)
{
float xWeight = sampler.GetValue(xK - pX);
Vector4 current = sourceBuffer.GetElementUnsafe(xK, yK).ToScaledVector4();
Numerics.Premultiply(ref current);
sum += current * xWeight * yWeight;
}
}
Numerics.UnPremultiply(ref sum);
span[x] = sum;
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
span,
rowSpan,
PixelConversionModifiers.Scale);
}
}
}
}

4
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs

@ -13,7 +13,7 @@ using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Points to a collection of of weights allocated in <see cref="ResizeKernelMap"/>.
/// Points to a collection of weights allocated in <see cref="ResizeKernelMap"/>.
/// </summary>
internal readonly unsafe struct ResizeKernel
{
@ -40,7 +40,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
}
/// <summary>
/// Gets the the length of the kernel.
/// Gets the length of the kernel.
/// </summary>
public int Length
{

28
tests/ImageSharp.Benchmarks/Processing/Rotate.cs

@ -21,21 +21,19 @@ namespace SixLabors.ImageSharp.Benchmarks.Processing
}
}
// #### 21th February 2020 ####
// #### 2021-04-06 ####
//
// BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18363
// Intel Core i7-8650U CPU 1.90GHz(Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
// .NET Core SDK = 3.1.101
// BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
// Intel Core i7-8650U CPU 1.90GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
// .NET Core SDK=5.0.201
// [Host] : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT
// Job-HQWHDJ : .NET Framework 4.8 (4.8.4341.0), X64 RyuJIT
// Job-RPXLFC : .NET Core 2.1.26 (CoreCLR 4.6.29812.02, CoreFX 4.6.29812.01), X64 RyuJIT
// Job-YMSKIM : .NET Core 3.1.13 (CoreCLR 4.700.21.11102, CoreFX 4.700.21.11602), X64 RyuJIT
//
// [Host] : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
// Job-HOGSNT : .NET Framework 4.8 (4.8.4121.0), X64 RyuJIT
// Job-FKDHXC : .NET Core 2.1.15 (CoreCLR 4.6.28325.01, CoreFX 4.6.28327.02), X64 RyuJIT
// Job-ODABAZ : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
//
// IterationCount=3 LaunchCount=1 WarmupCount=3
//
// | Method | Runtime | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
// |--------- |-------------- |---------:|---------:|---------:|------:|------:|------:|----------:|
// | DoRotate | .NET 4.7.2 | 28.77 ms | 3.304 ms | 0.181 ms | - | - | - | 6.5 KB |
// | DoRotate | .NET Core 2.1 | 16.27 ms | 1.044 ms | 0.057 ms | - | - | - | 5.25 KB |
// | DoRotate | .NET Core 3.1 | 17.12 ms | 4.352 ms | 0.239 ms | - | - | - | 6.57 KB |
// | Method | Job | Runtime | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
// |--------- |----------- |-------------- |---------:|---------:|---------:|------:|------:|------:|----------:|
// | DoRotate | Job-BAUEPW | .NET 4.7.2 | 30.73 ms | 0.397 ms | 0.331 ms | - | - | - | 6.75 KB |
// | DoRotate | Job-SNWMCN | .NET Core 2.1 | 16.31 ms | 0.317 ms | 0.352 ms | - | - | - | 5.25 KB |
// | DoRotate | Job-MRMBJZ | .NET Core 3.1 | 12.21 ms | 0.239 ms | 0.245 ms | - | - | - | 6.61 KB |

28
tests/ImageSharp.Benchmarks/Processing/Skew.cs

@ -21,21 +21,19 @@ namespace SixLabors.ImageSharp.Benchmarks.Processing
}
}
// #### 21th February 2020 ####
// #### 2021-04-06 ####
//
// BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18363
// Intel Core i7-8650U CPU 1.90GHz(Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
// .NET Core SDK = 3.1.101
// BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
// Intel Core i7-8650U CPU 1.90GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
// .NET Core SDK=5.0.201
// [Host] : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT
// Job-HQWHDJ : .NET Framework 4.8 (4.8.4341.0), X64 RyuJIT
// Job-RPXLFC : .NET Core 2.1.26 (CoreCLR 4.6.29812.02, CoreFX 4.6.29812.01), X64 RyuJIT
// Job-YMSKIM : .NET Core 3.1.13 (CoreCLR 4.700.21.11102, CoreFX 4.700.21.11602), X64 RyuJIT
//
// [Host] : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
// Job-VKKTMF : .NET Framework 4.8 (4.8.4121.0), X64 RyuJIT
// Job-KTVRKR : .NET Core 2.1.15 (CoreCLR 4.6.28325.01, CoreFX 4.6.28327.02), X64 RyuJIT
// Job-EONWDB : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
//
// IterationCount=3 LaunchCount=1 WarmupCount=3
//
// | Method | Runtime | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
// |------- |-------------- |---------:|----------:|---------:|------:|------:|------:|----------:|
// | DoSkew | .NET 4.7.2 | 24.60 ms | 33.971 ms | 1.862 ms | - | - | - | 6.5 KB |
// | DoSkew | .NET Core 2.1 | 12.13 ms | 2.256 ms | 0.124 ms | - | - | - | 5.21 KB |
// | DoSkew | .NET Core 3.1 | 12.83 ms | 1.442 ms | 0.079 ms | - | - | - | 6.57 KB |
// | Method | Job | Runtime | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
// |------- |----------- |-------------- |----------:|----------:|----------:|------:|------:|------:|----------:|
// | DoSkew | Job-YEGFRQ | .NET 4.7.2 | 23.563 ms | 0.0731 ms | 0.0570 ms | - | - | - | 6.75 KB |
// | DoSkew | Job-HZHOGR | .NET Core 2.1 | 13.700 ms | 0.2727 ms | 0.5122 ms | - | - | - | 5.25 KB |
// | DoSkew | Job-LTEUKY | .NET Core 3.1 | 9.971 ms | 0.0254 ms | 0.0225 ms | - | - | - | 6.61 KB |

31
tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs

@ -40,6 +40,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{ JpegSubsample.Ratio444, 100 },
};
public static readonly TheoryData<int> Grayscale_Quality =
new TheoryData<int>
{
{ 40 },
{ 60 },
{ 100 }
};
public static readonly TheoryData<string, int, int, PixelResolutionUnit> RatioFiles =
new TheoryData<string, int, int, PixelResolutionUnit>
{
@ -80,9 +88,20 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[WithSolidFilledImages(nameof(BitsPerPixel_Quality), 1, 1, 255, 100, 50, 255, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 7, 5, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 600, 400, PixelTypes.Rgba32)]
[WithSolidFilledImages(nameof(BitsPerPixel_Quality), 1, 1, 100, 100, 100, 255, PixelTypes.L8)]
public void EncodeBaseline_WorksWithDifferentSizes<TPixel>(TestImageProvider<TPixel> provider, JpegSubsample subsample, int quality)
where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, subsample, quality);
[Theory]
[WithFile(TestImages.Png.BikeGrayscale, nameof(Grayscale_Quality), PixelTypes.L8)]
[WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.Rgba32, 100)]
[WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.L8, 100)]
[WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.L16, 100)]
[WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.La16, 100)]
[WithSolidFilledImages(1, 1, 100, 100, 100, 255, PixelTypes.La32, 100)]
public void EncodeBaseline_Grayscale<TPixel>(TestImageProvider<TPixel> provider, int quality)
where TPixel : unmanaged, IPixel<TPixel> => TestJpegEncoderCore(provider, null, quality, JpegColorType.Luminance);
[Theory]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 48, 48, PixelTypes.Rgba32 | PixelTypes.Bgra32)]
public void EncodeBaseline_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, JpegSubsample subsample, int quality)
@ -101,13 +120,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
: ImageComparer.TolerantPercentage(5f);
provider.LimitAllocatorBufferCapacity().InBytesSqrt(200);
TestJpegEncoderCore(provider, subsample, 100, comparer);
TestJpegEncoderCore(provider, subsample, 100, JpegColorType.YCbCr, comparer);
}
/// <summary>
/// Anton's SUPER-SCIENTIFIC tolerance threshold calculation
/// </summary>
private static ImageComparer GetComparer(int quality, JpegSubsample subsample)
private static ImageComparer GetComparer(int quality, JpegSubsample? subsample)
{
float tolerance = 0.015f; // ~1.5%
@ -129,8 +148,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
private static void TestJpegEncoderCore<TPixel>(
TestImageProvider<TPixel> provider,
JpegSubsample subsample,
JpegSubsample? subsample,
int quality = 100,
JpegColorType colorType = JpegColorType.YCbCr,
ImageComparer comparer = null)
where TPixel : unmanaged, IPixel<TPixel>
{
@ -142,7 +162,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
var encoder = new JpegEncoder
{
Subsample = subsample,
Quality = quality
Quality = quality,
ColorType = colorType
};
string info = $"{subsample}-Q{quality}";
@ -298,7 +319,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
public async Task Encode_IsCancellable(JpegSubsample subsample, int cancellationDelayMs)
{
using var image = new Image<Rgba32>(5000, 5000);
using MemoryStream stream = new MemoryStream();
using var stream = new MemoryStream();
var cts = new CancellationTokenSource();
if (cancellationDelayMs == 0)
{

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

@ -12,12 +12,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Fact]
public void CloneIsDeep()
{
var meta = new JpegMetadata { Quality = 50 };
var meta = new JpegMetadata { Quality = 50, ColorType = JpegColorType.Luminance };
var clone = (JpegMetadata)meta.DeepClone();
clone.Quality = 99;
clone.ColorType = JpegColorType.YCbCr;
Assert.False(meta.Quality.Equals(clone.Quality));
Assert.False(meta.ColorType.Equals(clone.ColorType));
}
}
}

41
tests/ImageSharp.Tests/Helpers/RuntimeEnvironmentTests.cs

@ -0,0 +1,41 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Runtime.InteropServices;
using Xunit;
#pragma warning disable IDE0022 // Use expression body for methods
namespace SixLabors.ImageSharp.Tests.Helpers
{
public class RuntimeEnvironmentTests
{
[Fact]
public void CanDetectNetCore()
{
#if NET5_0_OR_GREATER
Assert.False(RuntimeEnvironment.IsNetCore);
#elif NETCOREAPP
Assert.True(RuntimeEnvironment.IsNetCore);
#else
Assert.False(RuntimeEnvironment.IsNetCore);
#endif
}
[Fact]
public void CanDetectOSPlatform()
{
if (TestEnvironment.IsLinux)
{
Assert.True(RuntimeEnvironment.IsOSPlatform(OSPlatform.Linux));
}
else if (TestEnvironment.IsOSX)
{
Assert.True(RuntimeEnvironment.IsOSPlatform(OSPlatform.OSX));
}
else if (TestEnvironment.IsWindows)
{
Assert.True(RuntimeEnvironment.IsOSPlatform(OSPlatform.Windows));
}
}
}
}

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

@ -4,7 +4,6 @@
using System;
using System.Numerics;
using System.Reflection;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
@ -18,9 +17,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
public class AffineTransformTests
{
private readonly ITestOutputHelper output;
// 1 byte difference on one color component.
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.0134F, 3);
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.033F, 3);
/// <summary>
/// angleDeg, sx, sy, tx, ty

2
tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs

@ -174,7 +174,7 @@ namespace SixLabors.ImageSharp.Tests
appendPixelTypeToFileName,
appendSourceFileOrDescription);
encoder = encoder ?? TestEnvironment.GetReferenceEncoder(path);
encoder ??= TestEnvironment.GetReferenceEncoder(path);
using (FileStream stream = File.OpenWrite(path))
{

94
tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs

@ -0,0 +1,94 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs
{
/// <summary>
/// A Png encoder that uses the ImageSharp core encoder but the default configuration.
/// This allows encoding under environments with restricted memory.
/// </summary>
public sealed class ImageSharpPngEncoderWithDefaultConfiguration : IImageEncoder, IPngEncoderOptions
{
/// <inheritdoc/>
public PngBitDepth? BitDepth { get; set; }
/// <inheritdoc/>
public PngColorType? ColorType { get; set; }
/// <inheritdoc/>
public PngFilterMethod? FilterMethod { get; set; }
/// <inheritdoc/>
public PngCompressionLevel CompressionLevel { get; set; } = PngCompressionLevel.DefaultCompression;
/// <inheritdoc/>
public int TextCompressionThreshold { get; set; } = 1024;
/// <inheritdoc/>
public float? Gamma { get; set; }
/// <inheritdoc/>
public IQuantizer Quantizer { get; set; }
/// <inheritdoc/>
public byte Threshold { get; set; } = byte.MaxValue;
/// <inheritdoc/>
public PngInterlaceMode? InterlaceMethod { get; set; }
/// <inheritdoc/>
public PngChunkFilter? ChunkFilter { get; set; }
/// <inheritdoc/>
public bool IgnoreMetadata { get; set; }
/// <inheritdoc/>
public PngTransparentColorMode TransparentColorMode { get; set; }
/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
Configuration configuration = Configuration.Default;
MemoryAllocator allocator = configuration.MemoryAllocator;
using var encoder = new PngEncoderCore(allocator, configuration, new PngEncoderOptions(this));
encoder.Encode(image, stream);
}
/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task EncodeAsync<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Configuration configuration = Configuration.Default;
MemoryAllocator allocator = configuration.MemoryAllocator;
// The introduction of a local variable that refers to an object the implements
// IDisposable means you must use async/await, where the compiler generates the
// state machine and a continuation.
using var encoder = new PngEncoderCore(allocator, configuration, new PngEncoderOptions(this));
await encoder.EncodeAsync(image, stream, cancellationToken).ConfigureAwait(false);
}
}
}

4
tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs

@ -59,10 +59,10 @@ namespace SixLabors.ImageSharp.Tests
new TgaConfigurationModule(),
new TiffConfigurationModule());
// Magick codecs should work on all platforms
IImageEncoder pngEncoder = IsWindows ? (IImageEncoder)SystemDrawingReferenceEncoder.Png : new PngEncoder();
IImageEncoder pngEncoder = IsWindows ? (IImageEncoder)SystemDrawingReferenceEncoder.Png : new ImageSharpPngEncoderWithDefaultConfiguration();
IImageEncoder bmpEncoder = IsWindows ? (IImageEncoder)SystemDrawingReferenceEncoder.Bmp : new BmpEncoder();
// Magick codecs should work on all platforms
cfg.ConfigureCodecs(
PngFormat.Instance,
MagickReferenceDecoder.Instance,

7
tests/ImageSharp.Tests/TestUtilities/TestEnvironment.cs

@ -26,6 +26,8 @@ namespace SixLabors.ImageSharp.Tests
private static readonly Lazy<bool> RunsOnCiLazy = new Lazy<bool>(() => bool.TryParse(Environment.GetEnvironmentVariable("CI"), out bool isCi) && isCi);
private static readonly Lazy<bool> RunsWithCodeCoverageLazy = new Lazy<bool>(() => bool.TryParse(Environment.GetEnvironmentVariable("codecov"), out bool isCodeCov) && isCodeCov);
private static readonly Lazy<string> NetCoreVersionLazy = new Lazy<string>(GetNetCoreVersion);
static TestEnvironment() => PrepareRemoteExecutor();
@ -42,6 +44,11 @@ namespace SixLabors.ImageSharp.Tests
/// </summary>
internal static bool RunsOnCI => RunsOnCiLazy.Value;
/// <summary>
/// Gets a value indicating whether test execution is running with code coverage testing enabled.
/// </summary>
internal static bool RunsWithCodeCoverage => RunsWithCodeCoverageLazy.Value;
internal static string SolutionDirectoryFullPath => SolutionDirectoryFullPathLazy.Value;
private static readonly FileInfo TestAssemblyFile =

61
tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs

@ -34,18 +34,16 @@ namespace SixLabors.ImageSharp.Tests
bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true,
IImageEncoder encoder = null)
{
image.DebugSave(
=> image.DebugSave(
provider,
(object)testOutputDetails,
extension,
appendPixelTypeToFileName,
appendSourceFileOrDescription,
encoder);
}
/// <summary>
/// Saves the image only when not running in the CI server.
/// Saves the image for debugging purpose.
/// </summary>
/// <param name="image">The image.</param>
/// <param name="provider">The image provider.</param>
@ -64,12 +62,11 @@ namespace SixLabors.ImageSharp.Tests
bool appendSourceFileOrDescription = true,
IImageEncoder encoder = null)
{
if (TestEnvironment.RunsOnCI)
if (TestEnvironment.RunsWithCodeCoverage)
{
return image;
}
// We are running locally then we want to save it out
provider.Utility.SaveTestOutputFile(
image,
extension,
@ -86,12 +83,10 @@ namespace SixLabors.ImageSharp.Tests
IImageEncoder encoder,
FormattableString testOutputDetails,
bool appendPixelTypeToFileName = true)
{
image.DebugSave(provider, encoder, (object)testOutputDetails, appendPixelTypeToFileName);
}
=> image.DebugSave(provider, encoder, (object)testOutputDetails, appendPixelTypeToFileName);
/// <summary>
/// Saves the image only when not running in the CI server.
/// Saves the image for debugging purpose.
/// </summary>
/// <param name="image">The image</param>
/// <param name="provider">The image provider</param>
@ -104,19 +99,11 @@ namespace SixLabors.ImageSharp.Tests
IImageEncoder encoder,
object testOutputDetails = null,
bool appendPixelTypeToFileName = true)
{
if (TestEnvironment.RunsOnCI)
{
return;
}
// We are running locally then we want to save it out
provider.Utility.SaveTestOutputFile(
=> provider.Utility.SaveTestOutputFile(
image,
encoder: encoder,
testOutputDetails: testOutputDetails,
appendPixelTypeToFileName: appendPixelTypeToFileName);
}
public static Image<TPixel> DebugSaveMultiFrame<TPixel>(
this Image<TPixel> image,
@ -126,17 +113,17 @@ namespace SixLabors.ImageSharp.Tests
bool appendPixelTypeToFileName = true)
where TPixel : unmanaged, IPixel<TPixel>
{
if (TestEnvironment.RunsOnCI)
if (TestEnvironment.RunsWithCodeCoverage)
{
return image;
}
// We are running locally then we want to save it out
provider.Utility.SaveTestOutputFileMultiFrame(
image,
extension,
testOutputDetails: testOutputDetails,
appendPixelTypeToFileName: appendPixelTypeToFileName);
return image;
}
@ -149,15 +136,13 @@ namespace SixLabors.ImageSharp.Tests
bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true)
where TPixel : unmanaged, IPixel<TPixel>
{
return image.CompareToReferenceOutput(
=> image.CompareToReferenceOutput(
provider,
(object)testOutputDetails,
extension,
grayscale,
appendPixelTypeToFileName,
appendSourceFileOrDescription);
}
/// <summary>
/// Compares the image against the expected Reference output, throws an exception if the images are not similar enough.
@ -181,8 +166,7 @@ namespace SixLabors.ImageSharp.Tests
bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true)
where TPixel : unmanaged, IPixel<TPixel>
{
return CompareToReferenceOutput(
=> CompareToReferenceOutput(
image,
ImageComparer.Tolerant(),
provider,
@ -191,7 +175,6 @@ namespace SixLabors.ImageSharp.Tests
grayscale,
appendPixelTypeToFileName,
appendSourceFileOrDescription);
}
public static Image<TPixel> CompareToReferenceOutput<TPixel>(
this Image<TPixel> image,
@ -202,15 +185,13 @@ namespace SixLabors.ImageSharp.Tests
bool grayscale = false,
bool appendPixelTypeToFileName = true)
where TPixel : unmanaged, IPixel<TPixel>
{
return image.CompareToReferenceOutput(
=> image.CompareToReferenceOutput(
comparer,
provider,
(object)testOutputDetails,
extension,
grayscale,
appendPixelTypeToFileName);
}
/// <summary>
/// Compares the image against the expected Reference output, throws an exception if the images are not similar enough.
@ -263,8 +244,7 @@ namespace SixLabors.ImageSharp.Tests
bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true)
where TPixel : unmanaged, IPixel<TPixel>
{
return image.CompareFirstFrameToReferenceOutput(
=> image.CompareFirstFrameToReferenceOutput(
comparer,
provider,
(object)testOutputDetails,
@ -272,7 +252,6 @@ namespace SixLabors.ImageSharp.Tests
grayscale,
appendPixelTypeToFileName,
appendSourceFileOrDescription);
}
public static Image<TPixel> CompareFirstFrameToReferenceOutput<TPixel>(
this Image<TPixel> image,
@ -508,9 +487,7 @@ namespace SixLabors.ImageSharp.Tests
ITestImageProvider provider,
IImageDecoder referenceDecoder = null)
where TPixel : unmanaged, IPixel<TPixel>
{
return CompareToOriginal(image, provider, ImageComparer.Tolerant(), referenceDecoder);
}
=> CompareToOriginal(image, provider, ImageComparer.Tolerant(), referenceDecoder);
public static Image<TPixel> CompareToOriginal<TPixel>(
this Image<TPixel> image,
@ -609,14 +586,12 @@ namespace SixLabors.ImageSharp.Tests
bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true)
where TPixel : unmanaged, IPixel<TPixel>
{
provider.VerifyOperation(
=> provider.VerifyOperation(
ImageComparer.Tolerant(),
operation,
testOutputDetails,
appendPixelTypeToFileName,
appendSourceFileOrDescription);
}
/// <summary>
/// Utility method for doing the following in one step:
@ -631,14 +606,12 @@ namespace SixLabors.ImageSharp.Tests
bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true)
where TPixel : unmanaged, IPixel<TPixel>
{
provider.VerifyOperation(
=> provider.VerifyOperation(
comparer,
operation,
$"",
appendPixelTypeToFileName,
appendSourceFileOrDescription);
}
/// <summary>
/// Utility method for doing the following in one step:
@ -652,9 +625,7 @@ namespace SixLabors.ImageSharp.Tests
bool appendPixelTypeToFileName = true,
bool appendSourceFileOrDescription = true)
where TPixel : unmanaged, IPixel<TPixel>
{
provider.VerifyOperation(operation, $"", appendPixelTypeToFileName, appendSourceFileOrDescription);
}
=> provider.VerifyOperation(operation, $"", appendPixelTypeToFileName, appendSourceFileOrDescription);
/// <summary>
/// Loads the expected image with a reference decoder + compares it to <paramref name="image"/>.

3
tests/ImageSharp.Tests/TestUtilities/Tests/TestEnvironmentTests.cs

@ -8,7 +8,6 @@ using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
using Xunit;
@ -85,7 +84,7 @@ namespace SixLabors.ImageSharp.Tests
}
[Theory]
[InlineData("lol/foo.png", typeof(PngEncoder))]
[InlineData("lol/foo.png", typeof(ImageSharpPngEncoderWithDefaultConfiguration))]
[InlineData("lol/Rofl.bmp", typeof(BmpEncoder))]
[InlineData("lol/Baz.JPG", typeof(JpegEncoder))]
[InlineData("lol/Baz.gif", typeof(GifEncoder))]

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:57376aa4446fc2d034d2a0cb627163ac416d1b6768b063b2c11ccf8517443bda
size 10135
oid sha256:2ef489dc0837b382ad7c7ead6b7c7042dfbfba39902d4cc81b5f3805d5b03967
size 9175

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9c83b59471a50df9e1d7b8f0e35c50aa417cd3be1730d6369f47f5cc99b87cef
size 6405
oid sha256:99d6c1d6b092a2feba2aebe2e09c521c3cc9682f3d748927cdc3cbaa38448b28
size 710

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f9f149093c5d9d487f3e16cb59e6807ea6ce581b5c307e57aeb4edaf921de9ca
size 15138
oid sha256:04a3b84c668758b586f4d998f080ef96b09f726b0298b28f5dd3d739b0e90744
size 13138

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f9f149093c5d9d487f3e16cb59e6807ea6ce581b5c307e57aeb4edaf921de9ca
size 15138
oid sha256:04a3b84c668758b586f4d998f080ef96b09f726b0298b28f5dd3d739b0e90744
size 13138

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0c7467702a784807a3a5813a75d5f643655ed5ad427e978d6ee079da67b05961
size 15363
oid sha256:6ea7ca66c31474c0bb9673a0d85c1c7465e387ebabf4e2d1e8f9daebfc7c8f34
size 13956

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a836c63c0912f69683bc9c8f59687b11e6b84f257816a01ff979ad0b6f4ab656
size 19059
oid sha256:6dd98ac441f3c20ea999f058c7b21601d5981d46e9b77709c25f2930a64edb93
size 17148

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:05de30dc282a0b8a096a476f689a1d2b6bb298098692a2f665c59c3d14902aa6
size 20426
oid sha256:d5c4772d9b9dc57c4b5d47450ec9d02d96e40656cf2015f171b5425945f8a598
size 18726

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8892179d8edf96583900048bdd895eff24e57be0105e91efafe1de971414db0e
size 22457
oid sha256:bb025c4470cec1b0d6544924e46b84cbdb90d75da5d0f879f2c7d7ec9875dee2
size 20574

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2f351ec6ae6df56537e383494984c2fbcf35a66c44a6ce53d6fd8d6d74a330f3
size 15342
oid sha256:c4abaa06827cb779026f8fbb655692bdd8adab37ae5b00c3ae18ebea456eb8d9
size 13459

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b0fb38dbdded32a1518d62ac79f4aa88133aaddad947f23c1066dc33d6938e0b
size 15372
oid sha256:3435ade8f7988779280820342e16881b049f717735d2218ac5a971a1bd807db1
size 13448

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0be42a5fd4ff10680af74a99ed1e0561ae38181f4efe4641bd63891222dcdf3c
size 15283
oid sha256:f0098aa45e820544dd16a58396fa70860886b7d79900916ed97376a8328a5ff2
size 13367

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1eabaf35e0dda3eccd5e8866ddd65e0c45557c8d5cc29423a99e2f377ee1bfa9
size 16271
oid sha256:7729495277b80a42e24dd8f40cdd8a280443575710fb4e199e4871c28b558271
size 14253

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5ab9bea8f45e7d29d7a8c5e3d0182c0f3f3aa7014aa358883dee53db6dfeb3f7
size 14076
oid sha256:a2304c234b93bdabaa018263dec70e62090ad1bbb7005ef62643b88600a863fb
size 12157

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ea452bc46c508f6990870a34d908224a58aa350c4ccebabd4fa6ba138e8034a0
size 18383
oid sha256:54b0da9646b7f4cf83df784d69dfbec48e0bdc1788d70a9872817543f72f57c1
size 16829

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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2c83df7a2f70aec8f150799055ce42db09568b47b95216c91a79233ce69381d5
size 191563
oid sha256:233d8c6c9e101dddf5d210d47c7a20807f8f956738289068ea03b774258ef8c6
size 182754

Loading…
Cancel
Save