Browse Source

Merge remote-tracking branch 'refs/remotes/origin/master' into feature/icc

af/merge-core
James Jackson-South 9 years ago
parent
commit
80b492d589
  1. 109
      src/ImageSharp/Formats/Jpeg/Components/Decoder/YCbCrToRgbTables.cs
  2. 131
      src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbToYCbCrTables.cs
  3. 82
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  4. 160
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  5. 498
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  6. 4
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  7. 2
      src/ImageSharp/Formats/Png/PngHeader.cs
  8. 4
      src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs
  9. 199
      src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs
  10. 18
      src/ImageSharp/Processing/Effects/BackgroundColor.cs
  11. 6
      src/ImageSharp/Processing/Effects/Brightness.cs
  12. 2
      src/ImageSharp/Processing/Processors/ColorMatrix/ColorMatrixProcessor.cs
  13. 9
      src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor.cs
  14. 13
      src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor.cs
  15. 88
      src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs
  16. 122
      src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor.cs
  17. 65
      src/ImageSharp/Processing/Processors/Effects/PixelateProcessor.cs
  18. 19
      src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.Weights.cs
  19. 8
      src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs
  20. 4
      tests/ImageSharp.Tests/Colors/Rgba32Tests.cs
  21. 0
      tests/ImageSharp.Tests/Colors/Rgba32TransformTests.cs
  22. 4
      tests/ImageSharp.Tests/Colors/RgbaVectorTests.cs
  23. 43
      tests/ImageSharp.Tests/Colors/RgbaVectorTransformTests.cs
  24. 1
      tests/ImageSharp.Tests/FileTestBase.cs
  25. 18
      tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
  26. 14
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  27. 20
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  28. 24
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  29. 8
      tests/ImageSharp.Tests/Formats/Png/PngSmokeTests.cs
  30. 5
      tests/ImageSharp.Tests/ImageComparer.cs
  31. 2
      tests/ImageSharp.Tests/Processors/Filters/AlphaTest.cs
  32. 16
      tests/ImageSharp.Tests/Processors/Filters/BackgroundColorTest.cs
  33. 17
      tests/ImageSharp.Tests/Processors/Filters/BinaryThresholdTest.cs
  34. 16
      tests/ImageSharp.Tests/Processors/Filters/BlackWhiteTest.cs
  35. 17
      tests/ImageSharp.Tests/Processors/Filters/BoxBlurTest.cs
  36. 17
      tests/ImageSharp.Tests/Processors/Filters/BrightnessTest.cs
  37. 17
      tests/ImageSharp.Tests/Processors/Filters/ColorBlindnessTest.cs
  38. 17
      tests/ImageSharp.Tests/Processors/Filters/ContrastTest.cs
  39. 38
      tests/ImageSharp.Tests/Processors/Filters/GaussianBlurTest.cs
  40. 36
      tests/ImageSharp.Tests/Processors/Filters/GaussianSharpenTest.cs
  41. 28
      tests/ImageSharp.Tests/Processors/Filters/GrayscaleTest.cs
  42. 17
      tests/ImageSharp.Tests/Processors/Filters/HueTest.cs
  43. 16
      tests/ImageSharp.Tests/Processors/Filters/KodachromeTest.cs
  44. 79
      tests/ImageSharp.Tests/Processors/Filters/PixelateTest.cs
  45. 16
      tests/ImageSharp.Tests/Processors/Filters/PolaroidTest.cs
  46. 17
      tests/ImageSharp.Tests/Processors/Filters/SaturationTest.cs
  47. 16
      tests/ImageSharp.Tests/Processors/Filters/SepiaTest.cs
  48. 1
      tests/ImageSharp.Tests/TestImages.cs
  49. 3
      tests/ImageSharp.Tests/TestImages/Formats/Png/cross.png
  50. 118
      tests/ImageSharp.Tests/TestUtilities/Attributes/ImageDataAttributeBase.cs
  51. 16
      tests/ImageSharp.Tests/TestUtilities/Attributes/WithBlankImageAttribute.cs
  52. 15
      tests/ImageSharp.Tests/TestUtilities/Attributes/WithFileAttribute.cs
  53. 19
      tests/ImageSharp.Tests/TestUtilities/Attributes/WithFileCollectionAttribute.cs
  54. 2
      tests/ImageSharp.Tests/TestUtilities/Attributes/WithMemberFactoryAttribute.cs
  55. 14
      tests/ImageSharp.Tests/TestUtilities/Attributes/WithTestPatternImageAttribute.cs
  56. 8
      tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs
  57. 4
      tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs
  58. 17
      tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs

109
src/ImageSharp/Formats/Jpeg/Components/Decoder/YCbCrToRgbTables.cs

@ -0,0 +1,109 @@
// <copyright file="YCbCrToRgbTables.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Formats.Jpg
{
using System.Runtime.CompilerServices;
using ImageSharp.PixelFormats;
/// <summary>
/// Provides 8-bit lookup tables for converting from YCbCr to Rgb colorspace.
/// Methods to build the tables are based on libjpeg implementation.
/// </summary>
internal unsafe struct YCbCrToRgbTables
{
/// <summary>
/// The red red-chrominance table
/// </summary>
public fixed int CrRTable[256];
/// <summary>
/// The blue blue-chrominance table
/// </summary>
public fixed int CbBTable[256];
/// <summary>
/// The green red-chrominance table
/// </summary>
public fixed int CrGTable[256];
/// <summary>
/// The green blue-chrominance table
/// </summary>
public fixed int CbGTable[256];
// Speediest right-shift on some machines and gives us enough accuracy at 4 decimal places.
private const int ScaleBits = 16;
private const int Half = 1 << (ScaleBits - 1);
/// <summary>
/// Initializes the YCbCr tables
/// </summary>
/// <returns>The intialized <see cref="YCbCrToRgbTables"/></returns>
public static YCbCrToRgbTables Create()
{
YCbCrToRgbTables tables = default(YCbCrToRgbTables);
for (int i = 0, x = -128; i <= 255; i++, x++)
{
// i is the actual input pixel value, in the range 0..255
// The Cb or Cr value we are thinking of is x = i - 128
// Cr=>R value is nearest int to 1.402 * x
tables.CrRTable[i] = RightShift((Fix(1.402F) * x) + Half);
// Cb=>B value is nearest int to 1.772 * x
tables.CbBTable[i] = RightShift((Fix(1.772F) * x) + Half);
// Cr=>G value is scaled-up -0.714136286
tables.CrGTable[i] = (-Fix(0.714136286F)) * x;
// Cb => G value is scaled - up - 0.344136286 * x
// We also add in Half so that need not do it in inner loop
tables.CbGTable[i] = ((-Fix(0.344136286F)) * x) + Half;
}
return tables;
}
/// <summary>
/// Optimized method to pack bytes to the image from the YCbCr color space.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="packed">The packed pixel.</param>
/// <param name="tables">The reference to the tables instance.</param>
/// <param name="y">The y luminance component.</param>
/// <param name="cb">The cb chroma component.</param>
/// <param name="cr">The cr chroma component.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Pack<TPixel>(ref TPixel packed, YCbCrToRgbTables* tables, byte y, byte cb, byte cr)
where TPixel : struct, IPixel<TPixel>
{
// float r = MathF.Round(y + (1.402F * cr), MidpointRounding.AwayFromZero);
byte r = (byte)(y + tables->CrRTable[cr]).Clamp(0, 255);
// float g = MathF.Round(y - (0.344136F * cb) - (0.714136F * cr), MidpointRounding.AwayFromZero);
// The values for the G calculation are left scaled up, since we must add them together before rounding.
byte g = (byte)(y + RightShift(tables->CbGTable[cb] + tables->CrGTable[cr])).Clamp(0, 255);
// float b = MathF.Round(y + (1.772F * cb), MidpointRounding.AwayFromZero);
byte b = (byte)(y + tables->CbBTable[cb]).Clamp(0, 255);
packed.PackFromBytes(r, g, b, byte.MaxValue);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int Fix(float x)
{
return (int)((x * (1L << ScaleBits)) + 0.5F);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int RightShift(int x)
{
return x >> ScaleBits;
}
}
}

131
src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbToYCbCrTables.cs

@ -0,0 +1,131 @@
// <copyright file="RgbToYCbCrTables.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Formats.Jpg
{
using System.Runtime.CompilerServices;
/// <summary>
/// Provides 8-bit lookup tables for converting from Rgb to YCbCr colorspace.
/// Methods to build the tables are based on libjpeg implementation.
/// </summary>
internal unsafe struct RgbToYCbCrTables
{
/// <summary>
/// The red luminance table
/// </summary>
public fixed int YRTable[256];
/// <summary>
/// The green luminance table
/// </summary>
public fixed int YGTable[256];
/// <summary>
/// The blue luminance table
/// </summary>
public fixed int YBTable[256];
/// <summary>
/// The red blue-chrominance table
/// </summary>
public fixed int CbRTable[256];
/// <summary>
/// The green blue-chrominance table
/// </summary>
public fixed int CbGTable[256];
/// <summary>
/// The blue blue-chrominance table
/// B=>Cb and R=>Cr are the same
/// </summary>
public fixed int CbBTable[256];
/// <summary>
/// The green red-chrominance table
/// </summary>
public fixed int CrGTable[256];
/// <summary>
/// The blue red-chrominance table
/// </summary>
public fixed int CrBTable[256];
// Speediest right-shift on some machines and gives us enough accuracy at 4 decimal places.
private const int ScaleBits = 16;
private const int CBCrOffset = 128 << ScaleBits;
private const int Half = 1 << (ScaleBits - 1);
/// <summary>
/// Initializes the YCbCr tables
/// </summary>
/// <returns>The intialized <see cref="RgbToYCbCrTables"/></returns>
public static RgbToYCbCrTables Create()
{
RgbToYCbCrTables tables = default(RgbToYCbCrTables);
for (int i = 0; i <= 255; i++)
{
// The values for the calculations are left scaled up since we must add them together before rounding.
tables.YRTable[i] = Fix(0.299F) * i;
tables.YGTable[i] = Fix(0.587F) * i;
tables.YBTable[i] = (Fix(0.114F) * i) + Half;
tables.CbRTable[i] = (-Fix(0.168735892F)) * i;
tables.CbGTable[i] = (-Fix(0.331264108F)) * i;
// We use a rounding fudge - factor of 0.5 - epsilon for Cb and Cr.
// This ensures that the maximum output will round to 255
// not 256, and thus that we don't have to range-limit.
//
// B=>Cb and R=>Cr tables are the same
tables.CbBTable[i] = (Fix(0.5F) * i) + CBCrOffset + Half - 1;
tables.CrGTable[i] = (-Fix(0.418687589F)) * i;
tables.CrBTable[i] = (-Fix(0.081312411F)) * i;
}
return tables;
}
/// <summary>
/// Optimized method to allocates the correct y, cb, and cr values to the DCT blocks from the given r, g, b values.
/// </summary>
/// <param name="yBlockRaw">The The luminance block.</param>
/// <param name="cbBlockRaw">The red chroma block.</param>
/// <param name="crBlockRaw">The blue chroma block.</param>
/// <param name="tables">The reference to the tables instance.</param>
/// <param name="index">The current index.</param>
/// <param name="r">The red value.</param>
/// <param name="g">The green value.</param>
/// <param name="b">The blue value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Allocate(ref float* yBlockRaw, ref float* cbBlockRaw, ref float* crBlockRaw, ref RgbToYCbCrTables* tables, int index, int r, int g, int b)
{
// float y = (0.299F * r) + (0.587F * g) + (0.114F * b);
yBlockRaw[index] = (tables->YRTable[r] + tables->YGTable[g] + tables->YBTable[b]) >> ScaleBits;
// float cb = 128F + ((-0.168736F * r) - (0.331264F * g) + (0.5F * b));
cbBlockRaw[index] = (tables->CbRTable[r] + tables->CbGTable[g] + tables->CbBTable[b]) >> ScaleBits;
// float b = MathF.Round(y + (1.772F * cb), MidpointRounding.AwayFromZero);
crBlockRaw[index] = (tables->CbBTable[r] + tables->CrGTable[g] + tables->CrBTable[b]) >> ScaleBits;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int Fix(float x)
{
return (int)((x * (1L << ScaleBits)) + 0.5F);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int RightShift(int x)
{
return x >> ScaleBits;
}
}
}

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

@ -38,6 +38,11 @@ namespace ImageSharp.Formats
public InputProcessor InputProcessor;
#pragma warning restore SA401
/// <summary>
/// Lookup tables for converting YCbCr to Rgb
/// </summary>
private static YCbCrToRgbTables yCbCrToRgbTables = YCbCrToRgbTables.Create();
/// <summary>
/// The decoder options.
/// </summary>
@ -256,35 +261,6 @@ namespace ImageSharp.Formats
}
}
/// <summary>
/// Optimized method to pack bytes to the image from the YCbCr color space.
/// This is faster than implicit casting as it avoids double packing.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="packed">The packed pixel.</param>
/// <param name="y">The y luminance component.</param>
/// <param name="cb">The cb chroma component.</param>
/// <param name="cr">The cr chroma component.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void PackYcbCr<TPixel>(ref TPixel packed, byte y, byte cb, byte cr)
where TPixel : struct, IPixel<TPixel>
{
int ccb = cb - 128;
int ccr = cr - 128;
// Speed up the algorithm by removing floating point calculation
// Scale by 65536, add .5F and truncate value. We use bit shifting to divide the result
int r0 = 91881 * ccr; // (1.402F * 65536) + .5F
int g0 = 22554 * ccb; // (0.34414F * 65536) + .5F
int g1 = 46802 * ccr; // (0.71414F * 65536) + .5F
int b0 = 116130 * ccb; // (1.772F * 65536) + .5F
byte r = (byte)(y + (r0 >> 16)).Clamp(0, 255);
byte g = (byte)(y - (g0 >> 16) - (g1 >> 16)).Clamp(0, 255);
byte b = (byte)(y + (b0 >> 16)).Clamp(0, 255);
packed.PackFromBytes(r, g, b, 255);
}
/// <summary>
/// Read metadata from stream and read the blocks in the scans into <see cref="DecodedBlocks"/>.
/// </summary>
@ -713,26 +689,34 @@ namespace ImageSharp.Formats
using (PixelAccessor<TPixel> pixels = image.Lock())
{
Parallel.For(
0,
image.Height,
image.Configuration.ParallelOptions,
y =>
{
// TODO: Simplify + optimize + share duplicate code across converter methods
int yo = this.ycbcrImage.GetRowYOffset(y);
int co = this.ycbcrImage.GetRowCOffset(y);
for (int x = 0; x < image.Width; x++)
{
byte yy = this.ycbcrImage.YChannel.Pixels[yo + x];
byte cb = this.ycbcrImage.CbChannel.Pixels[co + (x / scale)];
byte cr = this.ycbcrImage.CrChannel.Pixels[co + (x / scale)];
TPixel packed = default(TPixel);
PackYcbCr<TPixel>(ref packed, yy, cb, cr);
pixels[x, y] = packed;
}
});
0,
image.Height,
image.Configuration.ParallelOptions,
y =>
{
// TODO. This Parallel loop doesn't give us the boost it should.
ref byte ycRef = ref this.ycbcrImage.YChannel.Pixels[0];
ref byte cbRef = ref this.ycbcrImage.CbChannel.Pixels[0];
ref byte crRef = ref this.ycbcrImage.CrChannel.Pixels[0];
fixed (YCbCrToRgbTables* tables = &yCbCrToRgbTables)
{
// TODO: Simplify + optimize + share duplicate code across converter methods
int yo = this.ycbcrImage.GetRowYOffset(y);
int co = this.ycbcrImage.GetRowCOffset(y);
for (int x = 0; x < image.Width; x++)
{
int cOff = co + (x / scale);
byte yy = Unsafe.Add(ref ycRef, yo + x);
byte cb = Unsafe.Add(ref cbRef, cOff);
byte cr = Unsafe.Add(ref crRef, cOff);
TPixel packed = default(TPixel);
YCbCrToRgbTables.Pack(ref packed, tables, yy, cb, cr);
pixels[x, y] = packed;
}
}
});
}
this.AssignResolution(image);

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

@ -5,11 +5,9 @@
namespace ImageSharp.Formats
{
using System;
using System.Buffers;
using System.IO;
using System.Runtime.CompilerServices;
using ImageSharp.Formats.Jpg;
using ImageSharp.Formats.Jpg.Components;
using ImageSharp.PixelFormats;
@ -103,6 +101,11 @@ namespace ImageSharp.Formats
}
};
/// <summary>
/// Lookup tables for converting Rgb to YCbCr
/// </summary>
private static RgbToYCbCrTables rgbToYCbCrTables = RgbToYCbCrTables.Create();
/// <summary>
/// A scratch buffer to reduce allocations.
/// </summary>
@ -285,6 +288,7 @@ namespace ImageSharp.Formats
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The pixel accessor.</param>
/// <param name="tables">The reference to the tables instance.</param>
/// <param name="x">The x-position within the image.</param>
/// <param name="y">The y-position within the image.</param>
/// <param name="yBlock">The luminance block.</param>
@ -293,6 +297,7 @@ namespace ImageSharp.Formats
/// <param name="rgbBytes">Temporal <see cref="PixelArea{TPixel}"/> provided by the caller</param>
private static void ToYCbCr<TPixel>(
PixelAccessor<TPixel> pixels,
RgbToYCbCrTables* tables,
int x,
int y,
Block8x8F* yBlock,
@ -321,29 +326,9 @@ namespace ImageSharp.Formats
int g = Unsafe.Add(ref data0, dataIdx + 1);
int b = Unsafe.Add(ref data0, dataIdx + 2);
// Speed up the algorithm by removing floating point calculation
// Scale by 65536, add .5F and truncate value. We use bit shifting to divide the result
int y0 = 19595 * r; // (0.299F * 65536) + .5F
int y1 = 38470 * g; // (0.587F * 65536) + .5F
int y2 = 7471 * b; // (0.114F * 65536) + .5F
int cb0 = -11057 * r; // (-0.168736F * 65536) + .5F
int cb1 = 21710 * g; // (0.331264F * 65536) + .5F
int cb2 = 32768 * b; // (0.5F * 65536) + .5F
int cr0 = 32768 * r; // (0.5F * 65536) + .5F
int cr1 = 27439 * g; // (0.418688F * 65536) + .5F
int cr2 = 5329 * b; // (0.081312F * 65536) + .5F
float yy = (y0 + y1 + y2) >> 16;
float cb = 128 + ((cb0 - cb1 + cb2) >> 16);
float cr = 128 + ((cr0 - cr1 - cr2) >> 16);
int index = j8 + i;
yBlockRaw[index] = yy;
cbBlockRaw[index] = cb;
crBlockRaw[index] = cr;
RgbToYCbCrTables.Allocate(ref yBlockRaw, ref cbBlockRaw, ref crBlockRaw, ref tables, index, r, g, b);
dataIdx += 3;
}
@ -464,38 +449,41 @@ namespace ImageSharp.Formats
// ReSharper disable once InconsistentNaming
int prevDCY = 0, prevDCCb = 0, prevDCCr = 0;
using (PixelArea<TPixel> rgbBytes = new PixelArea<TPixel>(8, 8, ComponentOrder.Xyz))
fixed (RgbToYCbCrTables* tables = &rgbToYCbCrTables)
{
for (int y = 0; y < pixels.Height; y += 8)
using (PixelArea<TPixel> rgbBytes = new PixelArea<TPixel>(8, 8, ComponentOrder.Xyz))
{
for (int x = 0; x < pixels.Width; x += 8)
for (int y = 0; y < pixels.Height; y += 8)
{
ToYCbCr(pixels, x, y, &b, &cb, &cr, rgbBytes);
prevDCY = this.WriteBlock(
QuantIndex.Luminance,
prevDCY,
&b,
&temp1,
&temp2,
&onStackLuminanceQuantTable,
unzig.Data);
prevDCCb = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCb,
&cb,
&temp1,
&temp2,
&onStackChrominanceQuantTable,
unzig.Data);
prevDCCr = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCr,
&cr,
&temp1,
&temp2,
&onStackChrominanceQuantTable,
unzig.Data);
for (int x = 0; x < pixels.Width; x += 8)
{
ToYCbCr(pixels, tables, x, y, &b, &cb, &cr, rgbBytes);
prevDCY = this.WriteBlock(
QuantIndex.Luminance,
prevDCY,
&b,
&temp1,
&temp2,
&onStackLuminanceQuantTable,
unzig.Data);
prevDCCb = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCb,
&cb,
&temp1,
&temp2,
&onStackChrominanceQuantTable,
unzig.Data);
prevDCCr = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCr,
&cr,
&temp1,
&temp2,
&onStackChrominanceQuantTable,
unzig.Data);
}
}
}
}
@ -837,49 +825,51 @@ namespace ImageSharp.Formats
// ReSharper disable once InconsistentNaming
int prevDCY = 0, prevDCCb = 0, prevDCCr = 0;
using (PixelArea<TPixel> rgbBytes = new PixelArea<TPixel>(8, 8, ComponentOrder.Xyz))
fixed (RgbToYCbCrTables* tables = &rgbToYCbCrTables)
{
for (int y = 0; y < pixels.Height; y += 16)
using (PixelArea<TPixel> rgbBytes = new PixelArea<TPixel>(8, 8, ComponentOrder.Xyz))
{
for (int x = 0; x < pixels.Width; x += 16)
for (int y = 0; y < pixels.Height; y += 16)
{
for (int i = 0; i < 4; i++)
for (int x = 0; x < pixels.Width; x += 16)
{
int xOff = (i & 1) * 8;
int yOff = (i & 2) * 4;
ToYCbCr(pixels, x + xOff, y + yOff, &b, cbPtr + i, crPtr + i, rgbBytes);
for (int i = 0; i < 4; i++)
{
int xOff = (i & 1) * 8;
int yOff = (i & 2) * 4;
ToYCbCr(pixels, tables, x + xOff, y + yOff, &b, cbPtr + i, crPtr + i, rgbBytes);
prevDCY = this.WriteBlock(
QuantIndex.Luminance,
prevDCY,
&b,
&temp1,
&temp2,
&onStackLuminanceQuantTable,
unzig.Data);
}
Block8x8F.Scale16X16To8X8(&b, cbPtr);
prevDCCb = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCb,
&b,
&temp1,
&temp2,
&onStackChrominanceQuantTable,
unzig.Data);
prevDCY = this.WriteBlock(
QuantIndex.Luminance,
prevDCY,
Block8x8F.Scale16X16To8X8(&b, crPtr);
prevDCCr = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCr,
&b,
&temp1,
&temp2,
&onStackLuminanceQuantTable,
&onStackChrominanceQuantTable,
unzig.Data);
}
Block8x8F.Scale16X16To8X8(&b, cbPtr);
prevDCCb = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCb,
&b,
&temp1,
&temp2,
&onStackChrominanceQuantTable,
unzig.Data);
Block8x8F.Scale16X16To8X8(&b, crPtr);
prevDCCr = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCr,
&b,
&temp1,
&temp2,
&onStackChrominanceQuantTable,
unzig.Data);
}
}
}

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

@ -24,7 +24,14 @@ namespace ImageSharp.Formats
/// <summary>
/// The dictionary of available color types.
/// </summary>
private static readonly Dictionary<int, byte[]> ColorTypes = new Dictionary<int, byte[]>();
private static readonly Dictionary<PngColorType, byte[]> ColorTypes = new Dictionary<PngColorType, byte[]>()
{
[PngColorType.Grayscale] = new byte[] { 1, 2, 4, 8 },
[PngColorType.Rgb] = new byte[] { 8 },
[PngColorType.Palette] = new byte[] { 1, 2, 4, 8 },
[PngColorType.GrayscaleWithAlpha] = new byte[] { 8 },
[PngColorType.RgbWithAlpha] = new byte[] { 8 },
};
/// <summary>
/// The amount to increment when processing each column per scanline for each interlaced pass
@ -122,20 +129,29 @@ namespace ImageSharp.Formats
private bool isEndChunkReached;
/// <summary>
/// Initializes static members of the <see cref="PngDecoderCore"/> class.
/// Previous scanline processed
/// </summary>
static PngDecoderCore()
{
ColorTypes.Add((int)PngColorType.Grayscale, new byte[] { 1, 2, 4, 8 });
private byte[] previousScanline;
ColorTypes.Add((int)PngColorType.Rgb, new byte[] { 8 });
/// <summary>
/// The current scanline that is being processed
/// </summary>
private byte[] scanline;
ColorTypes.Add((int)PngColorType.Palette, new byte[] { 1, 2, 4, 8 });
/// <summary>
/// The index of the current scanline being processed
/// </summary>
private int currentRow = Adam7FirstRow[0];
ColorTypes.Add((int)PngColorType.GrayscaleWithAlpha, new byte[] { 8 });
/// <summary>
/// The current pass for an interlaced PNG
/// </summary>
private int pass = 0;
ColorTypes.Add((int)PngColorType.RgbWithAlpha, new byte[] { 8 });
}
/// <summary>
/// The current number of bytes read in the current scanline
/// </summary>
private int currentRowBytesRead = 0;
/// <summary>
/// Initializes a new instance of the <see cref="PngDecoderCore"/> class.
@ -171,65 +187,76 @@ namespace ImageSharp.Formats
ImageMetaData metadata = new ImageMetaData();
this.currentStream = stream;
this.currentStream.Skip(8);
using (MemoryStream dataStream = new MemoryStream())
Image<TPixel> image = null;
PixelAccessor<TPixel> pixels = null;
try
{
PngChunk currentChunk;
while (!this.isEndChunkReached && (currentChunk = this.ReadChunk()) != null)
using (ZlibInflateStream deframeStream = new ZlibInflateStream(this.currentStream))
{
try
PngChunk currentChunk;
while (!this.isEndChunkReached && (currentChunk = this.ReadChunk()) != null)
{
switch (currentChunk.Type)
try
{
case PngChunkTypes.Header:
this.ReadHeaderChunk(currentChunk.Data);
this.ValidateHeader();
break;
case PngChunkTypes.Physical:
this.ReadPhysicalChunk(metadata, currentChunk.Data);
break;
case PngChunkTypes.Data:
dataStream.Write(currentChunk.Data, 0, currentChunk.Length);
break;
case PngChunkTypes.Palette:
byte[] pal = new byte[currentChunk.Length];
Buffer.BlockCopy(currentChunk.Data, 0, pal, 0, currentChunk.Length);
this.palette = pal;
metadata.Quality = pal.Length / 3;
break;
case PngChunkTypes.PaletteAlpha:
byte[] alpha = new byte[currentChunk.Length];
Buffer.BlockCopy(currentChunk.Data, 0, alpha, 0, currentChunk.Length);
this.paletteAlpha = alpha;
break;
case PngChunkTypes.Text:
this.ReadTextChunk(metadata, currentChunk.Data, currentChunk.Length);
break;
case PngChunkTypes.End:
this.isEndChunkReached = true;
break;
switch (currentChunk.Type)
{
case PngChunkTypes.Header:
this.ReadHeaderChunk(currentChunk.Data);
this.ValidateHeader();
break;
case PngChunkTypes.Physical:
this.ReadPhysicalChunk(metadata, currentChunk.Data);
break;
case PngChunkTypes.Data:
if (image == null)
{
this.InitializeImage(metadata, out image, out pixels);
}
deframeStream.AllocateNewBytes(currentChunk.Length);
this.ReadScanlines(deframeStream.CompressedStream, pixels);
stream.Read(this.crcBuffer, 0, 4);
break;
case PngChunkTypes.Palette:
byte[] pal = new byte[currentChunk.Length];
Buffer.BlockCopy(currentChunk.Data, 0, pal, 0, currentChunk.Length);
this.palette = pal;
metadata.Quality = pal.Length / 3;
break;
case PngChunkTypes.PaletteAlpha:
byte[] alpha = new byte[currentChunk.Length];
Buffer.BlockCopy(currentChunk.Data, 0, alpha, 0, currentChunk.Length);
this.paletteAlpha = alpha;
break;
case PngChunkTypes.Text:
this.ReadTextChunk(metadata, currentChunk.Data, currentChunk.Length);
break;
case PngChunkTypes.End:
this.isEndChunkReached = true;
break;
}
}
finally
{
// Data is rented in ReadChunkData()
if (currentChunk.Data != null)
{
ArrayPool<byte>.Shared.Return(currentChunk.Data);
}
}
}
finally
{
// Data is rented in ReadChunkData()
ArrayPool<byte>.Shared.Return(currentChunk.Data);
}
}
if (this.header.Width > Image<TPixel>.MaxWidth || this.header.Height > Image<TPixel>.MaxHeight)
{
throw new ArgumentOutOfRangeException($"The input png '{this.header.Width}x{this.header.Height}' is bigger than the max allowed size '{Image<TPixel>.MaxWidth}x{Image<TPixel>.MaxHeight}'");
}
Image<TPixel> image = Image.Create<TPixel>(this.header.Width, this.header.Height, metadata, this.configuration);
using (PixelAccessor<TPixel> pixels = image.Lock())
return image;
}
finally
{
pixels?.Dispose();
if (this.previousScanline != null)
{
this.ReadScanlines(dataStream, pixels);
ArrayPool<byte>.Shared.Return(this.previousScanline);
ArrayPool<byte>.Shared.Return(this.scanline);
}
return image;
}
}
@ -293,6 +320,39 @@ namespace ImageSharp.Formats
metadata.VerticalResolution = BitConverter.ToInt32(data, 4) / 39.3700787d;
}
/// <summary>
/// Initializes the image and various buffers needed for processing
/// </summary>
/// <typeparam name="TPixel">The type the pixels will be</typeparam>
/// <param name="metadata">The metadata information for the image</param>
/// <param name="image">The image that we will populate</param>
/// <param name="pixels">The pixel accessor</param>
private void InitializeImage<TPixel>(ImageMetaData metadata, out Image<TPixel> image, out PixelAccessor<TPixel> pixels)
where TPixel : struct, IPixel<TPixel>
{
if (this.header.Width > Image<TPixel>.MaxWidth || this.header.Height > Image<TPixel>.MaxHeight)
{
throw new ArgumentOutOfRangeException($"The input png '{this.header.Width}x{this.header.Height}' is bigger than the max allowed size '{Image<TPixel>.MaxWidth}x{Image<TPixel>.MaxHeight}'");
}
image = Image.Create<TPixel>(this.header.Width, this.header.Height, metadata, this.configuration);
pixels = image.Lock();
this.bytesPerPixel = this.CalculateBytesPerPixel();
this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1;
this.bytesPerSample = 1;
if (this.header.BitDepth >= 8)
{
this.bytesPerSample = this.header.BitDepth / 8;
}
this.previousScanline = ArrayPool<byte>.Shared.Rent(this.bytesPerScanline);
this.scanline = ArrayPool<byte>.Shared.Rent(this.bytesPerScanline);
// Zero out the scanlines, because the bytes that are rented from the arraypool may not be zero.
Array.Clear(this.scanline, 0, this.bytesPerScanline);
Array.Clear(this.previousScanline, 0, this.bytesPerScanline);
}
/// <summary>
/// Calculates the correct number of bytes per pixel for the given color type.
/// </summary>
@ -345,28 +405,16 @@ namespace ImageSharp.Formats
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="dataStream">The <see cref="MemoryStream"/> containing data.</param>
/// <param name="pixels"> The pixel data.</param>
private void ReadScanlines<TPixel>(MemoryStream dataStream, PixelAccessor<TPixel> pixels)
private void ReadScanlines<TPixel>(Stream dataStream, PixelAccessor<TPixel> pixels)
where TPixel : struct, IPixel<TPixel>
{
this.bytesPerPixel = this.CalculateBytesPerPixel();
this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1;
this.bytesPerSample = 1;
if (this.header.BitDepth >= 8)
if (this.header.InterlaceMethod == PngInterlaceMode.Adam7)
{
this.bytesPerSample = this.header.BitDepth / 8;
this.DecodeInterlacedPixelData(dataStream, pixels);
}
dataStream.Position = 0;
using (ZlibInflateStream compressedStream = new ZlibInflateStream(dataStream))
else
{
if (this.header.InterlaceMethod == PngInterlaceMode.Adam7)
{
this.DecodeInterlacedPixelData(compressedStream, pixels);
}
else
{
this.DecodePixelData(compressedStream, pixels);
}
this.DecodePixelData(dataStream, pixels);
}
}
@ -379,66 +427,58 @@ namespace ImageSharp.Formats
private void DecodePixelData<TPixel>(Stream compressedStream, PixelAccessor<TPixel> pixels)
where TPixel : struct, IPixel<TPixel>
{
byte[] previousScanline = ArrayPool<byte>.Shared.Rent(this.bytesPerScanline);
byte[] scanline = ArrayPool<byte>.Shared.Rent(this.bytesPerScanline);
// Zero out the scanlines, because the bytes that are rented from the arraypool may not be zero.
Array.Clear(scanline, 0, this.bytesPerScanline);
Array.Clear(previousScanline, 0, this.bytesPerScanline);
try
while (this.currentRow < this.header.Height)
{
for (int y = 0; y < this.header.Height; y++)
int bytesRead = compressedStream.Read(this.scanline, this.currentRowBytesRead, this.bytesPerScanline - this.currentRowBytesRead);
this.currentRowBytesRead += bytesRead;
if (this.currentRowBytesRead < this.bytesPerScanline)
{
compressedStream.Read(scanline, 0, this.bytesPerScanline);
return;
}
FilterType filterType = (FilterType)scanline[0];
this.currentRowBytesRead = 0;
FilterType filterType = (FilterType)this.scanline[0];
switch (filterType)
{
case FilterType.None:
switch (filterType)
{
case FilterType.None:
NoneFilter.Decode(scanline);
NoneFilter.Decode(this.scanline);
break;
break;
case FilterType.Sub:
case FilterType.Sub:
SubFilter.Decode(scanline, this.bytesPerScanline, this.bytesPerPixel);
SubFilter.Decode(this.scanline, this.bytesPerScanline, this.bytesPerPixel);
break;
break;
case FilterType.Up:
case FilterType.Up:
UpFilter.Decode(scanline, previousScanline, this.bytesPerScanline);
UpFilter.Decode(this.scanline, this.previousScanline, this.bytesPerScanline);
break;
break;
case FilterType.Average:
case FilterType.Average:
AverageFilter.Decode(scanline, previousScanline, this.bytesPerScanline, this.bytesPerPixel);
AverageFilter.Decode(this.scanline, this.previousScanline, this.bytesPerScanline, this.bytesPerPixel);
break;
break;
case FilterType.Paeth:
case FilterType.Paeth:
PaethFilter.Decode(scanline, previousScanline, this.bytesPerScanline, this.bytesPerPixel);
PaethFilter.Decode(this.scanline, this.previousScanline, this.bytesPerScanline, this.bytesPerPixel);
break;
break;
default:
throw new ImageFormatException("Unknown filter type.");
}
default:
throw new ImageFormatException("Unknown filter type.");
}
this.ProcessDefilteredScanline(scanline, y, pixels);
this.ProcessDefilteredScanline(this.scanline, pixels);
Swap(ref scanline, ref previousScanline);
}
}
finally
{
ArrayPool<byte>.Shared.Return(previousScanline);
ArrayPool<byte>.Shared.Return(scanline);
Swap(ref this.scanline, ref this.previousScanline);
this.currentRow++;
}
}
@ -452,82 +492,85 @@ namespace ImageSharp.Formats
private void DecodeInterlacedPixelData<TPixel>(Stream compressedStream, PixelAccessor<TPixel> pixels)
where TPixel : struct, IPixel<TPixel>
{
byte[] previousScanline = ArrayPool<byte>.Shared.Rent(this.bytesPerScanline);
byte[] scanline = ArrayPool<byte>.Shared.Rent(this.bytesPerScanline);
try
while (true)
{
for (int pass = 0; pass < 7; pass++)
int numColumns = this.ComputeColumnsAdam7(this.pass);
if (numColumns == 0)
{
// Zero out the scanlines, because the bytes that are rented from the arraypool may not be zero.
Array.Clear(scanline, 0, this.bytesPerScanline);
Array.Clear(previousScanline, 0, this.bytesPerScanline);
this.pass++;
int y = Adam7FirstRow[pass];
int numColumns = this.ComputeColumnsAdam7(pass);
// This pass contains no data; skip to next pass
continue;
}
int bytesPerInterlaceScanline = this.CalculateScanlineLength(numColumns) + 1;
if (numColumns == 0)
while (this.currentRow < this.header.Height)
{
int bytesRead = compressedStream.Read(this.scanline, this.currentRowBytesRead, bytesPerInterlaceScanline - this.currentRowBytesRead);
this.currentRowBytesRead += bytesRead;
if (this.currentRowBytesRead < bytesPerInterlaceScanline)
{
// This pass contains no data; skip to next pass
continue;
return;
}
int bytesPerInterlaceScanline = this.CalculateScanlineLength(numColumns) + 1;
this.currentRowBytesRead = 0;
while (y < this.header.Height)
{
compressedStream.Read(scanline, 0, bytesPerInterlaceScanline);
FilterType filterType = (FilterType)this.scanline[0];
FilterType filterType = (FilterType)scanline[0];
switch (filterType)
{
case FilterType.None:
switch (filterType)
{
case FilterType.None:
NoneFilter.Decode(this.scanline);
NoneFilter.Decode(scanline);
break;
break;
case FilterType.Sub:
case FilterType.Sub:
SubFilter.Decode(this.scanline, bytesPerInterlaceScanline, this.bytesPerPixel);
SubFilter.Decode(scanline, bytesPerInterlaceScanline, this.bytesPerPixel);
break;
break;
case FilterType.Up:
case FilterType.Up:
UpFilter.Decode(this.scanline, this.previousScanline, bytesPerInterlaceScanline);
UpFilter.Decode(scanline, previousScanline, bytesPerInterlaceScanline);
break;
break;
case FilterType.Average:
case FilterType.Average:
AverageFilter.Decode(this.scanline, this.previousScanline, bytesPerInterlaceScanline, this.bytesPerPixel);
AverageFilter.Decode(scanline, previousScanline, bytesPerInterlaceScanline, this.bytesPerPixel);
break;
break;
case FilterType.Paeth:
case FilterType.Paeth:
PaethFilter.Decode(this.scanline, this.previousScanline, bytesPerInterlaceScanline, this.bytesPerPixel);
PaethFilter.Decode(scanline, previousScanline, bytesPerInterlaceScanline, this.bytesPerPixel);
break;
break;
default:
throw new ImageFormatException("Unknown filter type.");
}
default:
throw new ImageFormatException("Unknown filter type.");
}
this.ProcessInterlacedDefilteredScanline(this.scanline, this.currentRow, pixels, Adam7FirstColumn[this.pass], Adam7ColumnIncrement[this.pass]);
this.ProcessInterlacedDefilteredScanline(scanline, y, pixels, Adam7FirstColumn[pass], Adam7ColumnIncrement[pass]);
Swap(ref this.scanline, ref this.previousScanline);
Swap(ref scanline, ref previousScanline);
this.currentRow += Adam7RowIncrement[this.pass];
}
y += Adam7RowIncrement[pass];
}
this.pass++;
if (this.pass < 7)
{
this.currentRow = Adam7FirstRow[this.pass];
}
else
{
break;
}
}
finally
{
ArrayPool<byte>.Shared.Return(previousScanline);
ArrayPool<byte>.Shared.Return(scanline);
}
}
@ -536,12 +579,13 @@ namespace ImageSharp.Formats
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="defilteredScanline">The de-filtered scanline</param>
/// <param name="row">The current image row.</param>
/// <param name="pixels">The image pixels</param>
private void ProcessDefilteredScanline<TPixel>(byte[] defilteredScanline, int row, PixelAccessor<TPixel> pixels)
private void ProcessDefilteredScanline<TPixel>(byte[] defilteredScanline, PixelAccessor<TPixel> pixels)
where TPixel : struct, IPixel<TPixel>
{
TPixel color = default(TPixel);
BufferSpan<TPixel> pixelBuffer = pixels.GetRowSpan(this.currentRow);
BufferSpan<byte> scanlineBuffer = new BufferSpan<byte>(defilteredScanline, 1);
switch (this.PngColorType)
{
case PngColorType.Grayscale:
@ -551,7 +595,7 @@ namespace ImageSharp.Formats
{
byte intensity = (byte)(newScanline1[x] * factor);
color.PackFromBytes(intensity, intensity, intensity, 255);
pixels[x, row] = color;
pixels[x, this.currentRow] = color;
}
break;
@ -566,91 +610,84 @@ namespace ImageSharp.Formats
byte alpha = defilteredScanline[offset + this.bytesPerSample];
color.PackFromBytes(intensity, intensity, intensity, alpha);
pixels[x, row] = color;
pixels[x, this.currentRow] = color;
}
break;
case PngColorType.Palette:
byte[] newScanline = ToArrayByBitsLength(defilteredScanline, this.bytesPerScanline, this.header.BitDepth);
this.ProcessScanlineFromPalette(defilteredScanline, pixels);
if (this.paletteAlpha != null && this.paletteAlpha.Length > 0)
{
// If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha
// channel and we should try to read it.
for (int x = 0; x < this.header.Width; x++)
{
int index = newScanline[x + 1];
int pixelOffset = index * 3;
break;
byte a = this.paletteAlpha.Length > index ? this.paletteAlpha[index] : (byte)255;
case PngColorType.Rgb:
if (a > 0)
{
byte r = this.palette[pixelOffset];
byte g = this.palette[pixelOffset + 1];
byte b = this.palette[pixelOffset + 2];
color.PackFromBytes(r, g, b, a);
}
else
{
color.PackFromBytes(0, 0, 0, 0);
}
BulkPixelOperations<TPixel>.Instance.PackFromXyzBytes(scanlineBuffer, pixelBuffer, this.header.Width);
pixels[x, row] = color;
}
}
else
{
for (int x = 0; x < this.header.Width; x++)
{
int index = newScanline[x + 1];
int pixelOffset = index * 3;
break;
byte r = this.palette[pixelOffset];
byte g = this.palette[pixelOffset + 1];
byte b = this.palette[pixelOffset + 2];
case PngColorType.RgbWithAlpha:
color.PackFromBytes(r, g, b, 255);
pixels[x, row] = color;
}
}
BulkPixelOperations<TPixel>.Instance.PackFromXyzwBytes(scanlineBuffer, pixelBuffer, this.header.Width);
break;
}
}
case PngColorType.Rgb:
/// <summary>
/// Processes a scanline that uses a palette
/// </summary>
/// <typeparam name="TPixel">The type of pixel we are expanding to</typeparam>
/// <param name="defilteredScanline">The scanline</param>
/// <param name="pixels">The output pixels</param>
private void ProcessScanlineFromPalette<TPixel>(byte[] defilteredScanline, PixelAccessor<TPixel> pixels)
where TPixel : struct, IPixel<TPixel>
{
byte[] newScanline = ToArrayByBitsLength(defilteredScanline, this.bytesPerScanline, this.header.BitDepth);
byte[] palette = this.palette;
TPixel color = default(TPixel);
for (int x = 0; x < this.header.Width; x++)
{
int offset = 1 + (x * this.bytesPerPixel);
if (this.paletteAlpha != null && this.paletteAlpha.Length > 0)
{
// If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha
// channel and we should try to read it.
for (int x = 0; x < this.header.Width; x++)
{
int index = newScanline[x + 1];
int pixelOffset = index * 3;
byte r = defilteredScanline[offset];
byte g = defilteredScanline[offset + this.bytesPerSample];
byte b = defilteredScanline[offset + (2 * this.bytesPerSample)];
byte a = this.paletteAlpha.Length > index ? this.paletteAlpha[index] : (byte)255;
color.PackFromBytes(r, g, b, 255);
pixels[x, row] = color;
if (a > 0)
{
byte r = palette[pixelOffset];
byte g = palette[pixelOffset + 1];
byte b = palette[pixelOffset + 2];
color.PackFromBytes(r, g, b, a);
}
break;
case PngColorType.RgbWithAlpha:
for (int x = 0; x < this.header.Width; x++)
else
{
int offset = 1 + (x * this.bytesPerPixel);
color.PackFromBytes(0, 0, 0, 0);
}
byte r = defilteredScanline[offset];
byte g = defilteredScanline[offset + this.bytesPerSample];
byte b = defilteredScanline[offset + (2 * this.bytesPerSample)];
byte a = defilteredScanline[offset + (3 * this.bytesPerSample)];
pixels[x, this.currentRow] = color;
}
}
else
{
for (int x = 0; x < this.header.Width; x++)
{
int index = newScanline[x + 1];
int pixelOffset = index * 3;
color.PackFromBytes(r, g, b, a);
pixels[x, row] = color;
}
byte r = palette[pixelOffset];
byte g = palette[pixelOffset + 1];
byte b = palette[pixelOffset + 2];
break;
color.PackFromBytes(r, g, b, 255);
pixels[x, this.currentRow] = color;
}
}
}
@ -819,7 +856,7 @@ namespace ImageSharp.Formats
this.header.Height = BitConverter.ToInt32(data, 4);
this.header.BitDepth = data[8];
this.header.ColorType = data[9];
this.header.ColorType = (PngColorType)data[9];
this.header.CompressionMethod = data[10];
this.header.FilterMethod = data[11];
this.header.InterlaceMethod = (PngInterlaceMode)data[12];
@ -872,6 +909,11 @@ namespace ImageSharp.Formats
}
this.ReadChunkType(chunk);
if (chunk.Type == PngChunkTypes.Data)
{
return chunk;
}
this.ReadChunkData(chunk);
this.ReadChunkCrc(chunk);

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

@ -181,7 +181,7 @@ namespace ImageSharp.Formats
{
Width = image.Width,
Height = image.Height,
ColorType = (byte)this.pngColorType,
ColorType = this.pngColorType,
BitDepth = this.bitDepth,
FilterMethod = 0, // None
CompressionMethod = 0,
@ -462,7 +462,7 @@ namespace ImageSharp.Formats
WriteInteger(this.chunkDataBuffer, 4, header.Height);
this.chunkDataBuffer[8] = header.BitDepth;
this.chunkDataBuffer[9] = header.ColorType;
this.chunkDataBuffer[9] = (byte)header.ColorType;
this.chunkDataBuffer[10] = header.CompressionMethod;
this.chunkDataBuffer[11] = header.FilterMethod;
this.chunkDataBuffer[12] = (byte)header.InterlaceMethod;

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

@ -34,7 +34,7 @@ namespace ImageSharp.Formats
/// image data. Color type codes represent sums of the following values:
/// 1 (palette used), 2 (color used), and 4 (alpha channel used).
/// </summary>
public byte ColorType { get; set; }
public PngColorType ColorType { get; set; }
/// <summary>
/// Gets or sets the compression method.

4
src/ImageSharp/Formats/Png/Zlib/ZlibDeflateStream.cs

@ -40,7 +40,7 @@ namespace ImageSharp.Formats
/// <summary>
/// The stream responsible for compressing the input stream.
/// </summary>
private DeflateStream deflateStream;
private System.IO.Compression.DeflateStream deflateStream;
/// <summary>
/// Initializes a new instance of the <see cref="ZlibDeflateStream"/> class.
@ -102,7 +102,7 @@ namespace ImageSharp.Formats
level = CompressionLevel.NoCompression;
}
this.deflateStream = new DeflateStream(this.rawStream, level, true);
this.deflateStream = new System.IO.Compression.DeflateStream(this.rawStream, level, true);
}
/// <inheritdoc/>

199
src/ImageSharp/Formats/Png/Zlib/ZlibInflateStream.cs

@ -1,23 +1,25 @@
// <copyright file="ZlibInflateStream.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Formats
namespace ImageSharp.Formats
{
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;
/// <summary>
/// Provides methods and properties for decompressing streams by using the Zlib Deflate algorithm.
/// Provides methods and properties for deframing streams from PNGs.
/// </summary>
internal sealed class ZlibInflateStream : Stream
internal class ZlibInflateStream : Stream
{
/// <summary>
/// The raw stream containing the uncompressed image data.
/// The inner raw memory stream
/// </summary>
private readonly Stream innerStream;
/// <summary>
/// The compressed stream sitting over the top of the deframer
/// </summary>
private readonly Stream rawStream;
private DeflateStream compressedStream;
/// <summary>
/// A value indicating whether this instance of the given entity has been disposed.
@ -38,123 +40,76 @@ namespace ImageSharp.Formats
private byte[] crcread;
/// <summary>
/// The stream responsible for decompressing the input stream.
/// The current data remaining to be read
/// </summary>
private DeflateStream deflateStream;
private int currentDataRemaining;
/// <summary>
/// Initializes a new instance of the <see cref="ZlibInflateStream"/> class.
/// </summary>
/// <param name="stream">The stream.</param>
/// <exception cref="Exception">
/// Thrown if the compression method is incorrect.
/// </exception>
public ZlibInflateStream(Stream stream)
/// <param name="innerStream">The inner raw stream</param>
public ZlibInflateStream(Stream innerStream)
{
// The DICT dictionary identifier identifying the used dictionary.
// The preset dictionary.
bool fdict;
this.rawStream = stream;
// Read the zlib header : http://tools.ietf.org/html/rfc1950
// CMF(Compression Method and flags)
// This byte is divided into a 4 - bit compression method and a
// 4-bit information field depending on the compression method.
// bits 0 to 3 CM Compression method
// bits 4 to 7 CINFO Compression info
//
// 0 1
// +---+---+
// |CMF|FLG|
// +---+---+
int cmf = this.rawStream.ReadByte();
int flag = this.rawStream.ReadByte();
if (cmf == -1 || flag == -1)
{
return;
}
if ((cmf & 0x0f) != 8)
{
throw new Exception($"Bad compression method for ZLIB header: cmf={cmf}");
}
// CINFO is the base-2 logarithm of the LZ77 window size, minus eight.
// int cinfo = ((cmf & (0xf0)) >> 8);
fdict = (flag & 32) != 0;
if (fdict)
{
// The DICT dictionary identifier identifying the used dictionary.
byte[] dictId = new byte[4];
for (int i = 0; i < 4; i++)
{
// We consume but don't use this.
dictId[i] = (byte)this.rawStream.ReadByte();
}
}
// Initialize the deflate Stream.
this.deflateStream = new DeflateStream(this.rawStream, CompressionMode.Decompress, true);
this.innerStream = innerStream;
}
/// <inheritdoc/>
public override bool CanRead => true;
public override bool CanRead => this.innerStream.CanRead;
/// <inheritdoc/>
public override bool CanSeek => false;
/// <inheritdoc/>
public override bool CanWrite => false;
public override bool CanWrite => throw new NotSupportedException();
/// <inheritdoc/>
public override long Length
public override long Length => throw new NotSupportedException();
/// <inheritdoc/>
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
/// <summary>
/// Gets the compressed stream over the deframed inner stream
/// </summary>
public DeflateStream CompressedStream => this.compressedStream;
/// <summary>
/// Adds new bytes from a frame found in the original stream
/// </summary>
/// <param name="bytes">blabla</param>
public void AllocateNewBytes(int bytes)
{
get
this.currentDataRemaining = bytes;
if (this.compressedStream == null)
{
throw new NotSupportedException();
this.InitializeInflateStream();
}
}
/// <inheritdoc/>
public override long Position
public override void Flush()
{
get
{
throw new NotSupportedException();
}
set
{
throw new NotSupportedException();
}
throw new NotSupportedException();
}
/// <inheritdoc/>
public override void Flush()
public override int ReadByte()
{
this.deflateStream?.Flush();
this.currentDataRemaining--;
return this.innerStream.ReadByte();
}
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
// We dont't check CRC on reading
int read = this.deflateStream.Read(buffer, offset, count);
if (read < 1 && this.crcread == null)
if (this.currentDataRemaining == 0)
{
// The deflater has ended. We try to read the next 4 bytes from raw stream (crc)
this.crcread = new byte[4];
for (int i = 0; i < 4; i++)
{
// we dont really check/use this
this.crcread[i] = (byte)this.rawStream.ReadByte();
}
return 0;
}
return read;
int bytesToRead = Math.Min(count, this.currentDataRemaining);
this.currentDataRemaining -= bytesToRead;
return this.innerStream.Read(buffer, offset, bytesToRead);
}
/// <inheritdoc/>
@ -186,10 +141,10 @@ namespace ImageSharp.Formats
if (disposing)
{
// dispose managed resources
if (this.deflateStream != null)
if (this.compressedStream != null)
{
this.deflateStream.Dispose();
this.deflateStream = null;
this.compressedStream.Dispose();
this.compressedStream = null;
if (this.crcread == null)
{
@ -197,7 +152,7 @@ namespace ImageSharp.Formats
this.crcread = new byte[4];
for (int i = 0; i < 4; i++)
{
this.crcread[i] = (byte)this.rawStream.ReadByte();
this.crcread[i] = (byte)this.innerStream.ReadByte();
}
}
}
@ -210,5 +165,57 @@ namespace ImageSharp.Formats
// Note disposing is done.
this.isDisposed = true;
}
private void InitializeInflateStream()
{
// The DICT dictionary identifier identifying the used dictionary.
// The preset dictionary.
bool fdict;
// Read the zlib header : http://tools.ietf.org/html/rfc1950
// CMF(Compression Method and flags)
// This byte is divided into a 4 - bit compression method and a
// 4-bit information field depending on the compression method.
// bits 0 to 3 CM Compression method
// bits 4 to 7 CINFO Compression info
//
// 0 1
// +---+---+
// |CMF|FLG|
// +---+---+
int cmf = this.innerStream.ReadByte();
int flag = this.innerStream.ReadByte();
this.currentDataRemaining -= 2;
if (cmf == -1 || flag == -1)
{
return;
}
if ((cmf & 0x0f) != 8)
{
throw new Exception($"Bad compression method for ZLIB header: cmf={cmf}");
}
// CINFO is the base-2 logarithm of the LZ77 window size, minus eight.
// int cinfo = ((cmf & (0xf0)) >> 8);
fdict = (flag & 32) != 0;
if (fdict)
{
// The DICT dictionary identifier identifying the used dictionary.
byte[] dictId = new byte[4];
for (int i = 0; i < 4; i++)
{
// We consume but don't use this.
dictId[i] = (byte)this.innerStream.ReadByte();
this.currentDataRemaining--;
}
}
// Initialize the deflate Stream.
this.compressedStream = new DeflateStream(this, CompressionMode.Decompress, true);
}
}
}

18
src/ImageSharp/Processing/Effects/BackgroundColor.cs

@ -26,7 +26,23 @@ namespace ImageSharp
public static Image<TPixel> BackgroundColor<TPixel>(this Image<TPixel> source, TPixel color)
where TPixel : struct, IPixel<TPixel>
{
source.ApplyProcessor(new BackgroundColorProcessor<TPixel>(color), source.Bounds);
return BackgroundColor(source, color, source.Bounds);
}
/// <summary>
/// Replaces the background color of image with the given one.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="color">The color to set as the background.</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.
/// </param>
/// <returns>The <see cref="Image"/>.</returns>
public static Image<TPixel> BackgroundColor<TPixel>(this Image<TPixel> source, TPixel color, Rectangle rectangle)
where TPixel : struct, IPixel<TPixel>
{
source.ApplyProcessor(new BackgroundColorProcessor<TPixel>(color), rectangle);
return source;
}
}

6
src/ImageSharp/Processing/Effects/Brightness.cs

@ -20,12 +20,12 @@ namespace ImageSharp
/// Alters the brightness component of the image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="source">The image this method extends.</param>
/// <param name="amount">The new brightness of the image. Must be between -100 and 100.</param>
/// <returns>The <see cref="Image{TPixel}"/>.</returns>
public static Image<TPixel> Brightness<TPixel>(this Image<TPixel> source, int amount)
where TPixel : struct, IPixel<TPixel>
{
{
return Brightness(source, amount, source.Bounds);
}
@ -33,7 +33,7 @@ namespace ImageSharp
/// Alters the brightness component of the image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="source">The image this method extends.</param>
/// <param name="amount">The new brightness of the image. Must be between -100 and 100.</param>
/// <param name="rectangle">
/// The <see cref="Rectangle"/> structure that specifies the portion of the image object to alter.

2
src/ImageSharp/Processing/Processors/ColorMatrix/ColorMatrixProcessor.cs

@ -7,6 +7,7 @@ namespace ImageSharp.Processing.Processors
{
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using ImageSharp.PixelFormats;
@ -79,6 +80,7 @@ namespace ImageSharp.Processing.Processors
/// <returns>
/// The <see cref="Rgba32"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private TPixel ApplyMatrix(TPixel color, Matrix4x4 matrix, bool compand)
{
Vector4 vector = color.ToVector4();

9
src/ImageSharp/Processing/Processors/Convolution/Convolution2DProcessor.cs

@ -5,7 +5,6 @@
namespace ImageSharp.Processing.Processors
{
using System;
using System.Numerics;
using System.Threading.Tasks;
@ -57,10 +56,11 @@ namespace ImageSharp.Processing.Processors
int maxX = endX - 1;
using (PixelAccessor<TPixel> targetPixels = new PixelAccessor<TPixel>(source.Width, source.Height))
using (PixelAccessor<TPixel> sourcePixels = source.Lock())
{
using (PixelAccessor<TPixel> sourcePixels = source.Lock())
{
Parallel.For(
sourcePixels.CopyTo(targetPixels);
Parallel.For(
startY,
endY,
this.ParallelOptions,
@ -119,7 +119,6 @@ namespace ImageSharp.Processing.Processors
targetPixels[x, y] = packed;
}
});
}
source.SwapPixelsBuffers(targetPixels);
}

13
src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor.cs

@ -45,16 +45,11 @@ namespace ImageSharp.Processing.Processors
int width = source.Width;
int height = source.Height;
using (PixelAccessor<TPixel> targetPixels = new PixelAccessor<TPixel>(width, height))
using (PixelAccessor<TPixel> firstPassPixels = new PixelAccessor<TPixel>(width, height))
using (PixelAccessor<TPixel> sourcePixels = source.Lock())
{
using (PixelAccessor<TPixel> firstPassPixels = new PixelAccessor<TPixel>(width, height))
using (PixelAccessor<TPixel> sourcePixels = source.Lock())
{
this.ApplyConvolution(firstPassPixels, sourcePixels, sourceRectangle, this.KernelX);
this.ApplyConvolution(targetPixels, firstPassPixels, sourceRectangle, this.KernelY);
}
source.SwapPixelsBuffers(targetPixels);
this.ApplyConvolution(firstPassPixels, sourcePixels, source.Bounds, this.KernelX);
this.ApplyConvolution(sourcePixels, firstPassPixels, sourceRectangle, this.KernelY);
}
}

88
src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor.cs

@ -46,51 +46,51 @@ namespace ImageSharp.Processing.Processors
int maxX = endX - 1;
using (PixelAccessor<TPixel> targetPixels = new PixelAccessor<TPixel>(source.Width, source.Height))
using (PixelAccessor<TPixel> sourcePixels = source.Lock())
{
using (PixelAccessor<TPixel> sourcePixels = source.Lock())
{
Parallel.For(
startY,
endY,
this.ParallelOptions,
y =>
{
for (int x = startX; x < endX; x++)
{
float red = 0;
float green = 0;
float blue = 0;
// Apply each matrix multiplier to the color components for each pixel.
for (int fy = 0; fy < kernelLength; fy++)
{
int fyr = fy - radius;
int offsetY = y + fyr;
offsetY = offsetY.Clamp(0, maxY);
for (int fx = 0; fx < kernelLength; fx++)
{
int fxr = fx - radius;
int offsetX = x + fxr;
offsetX = offsetX.Clamp(0, maxX);
Vector4 currentColor = sourcePixels[offsetX, offsetY].ToVector4();
currentColor *= this.KernelXY[fy, fx];
red += currentColor.X;
green += currentColor.Y;
blue += currentColor.Z;
}
}
TPixel packed = default(TPixel);
packed.PackFromVector4(new Vector4(red, green, blue, sourcePixels[x, y].ToVector4().W));
targetPixels[x, y] = packed;
}
});
}
sourcePixels.CopyTo(targetPixels);
Parallel.For(
startY,
endY,
this.ParallelOptions,
y =>
{
for (int x = startX; x < endX; x++)
{
float red = 0;
float green = 0;
float blue = 0;
// Apply each matrix multiplier to the color components for each pixel.
for (int fy = 0; fy < kernelLength; fy++)
{
int fyr = fy - radius;
int offsetY = y + fyr;
offsetY = offsetY.Clamp(0, maxY);
for (int fx = 0; fx < kernelLength; fx++)
{
int fxr = fx - radius;
int offsetX = x + fxr;
offsetX = offsetX.Clamp(0, maxX);
Vector4 currentColor = sourcePixels[offsetX, offsetY].ToVector4();
currentColor *= this.KernelXY[fy, fx];
red += currentColor.X;
green += currentColor.Y;
blue += currentColor.Z;
}
}
TPixel packed = default(TPixel);
packed.PackFromVector4(new Vector4(red, green, blue, sourcePixels[x, y].ToVector4().W));
targetPixels[x, y] = packed;
}
});
source.SwapPixelsBuffers(targetPixels);
}

122
src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor.cs

@ -70,88 +70,88 @@ namespace ImageSharp.Processing.Processors
}
using (PixelAccessor<TPixel> targetPixels = new PixelAccessor<TPixel>(source.Width, source.Height))
using (PixelAccessor<TPixel> sourcePixels = source.Lock())
{
using (PixelAccessor<TPixel> sourcePixels = source.Lock())
{
Parallel.For(
minY,
maxY,
this.ParallelOptions,
y =>
sourcePixels.CopyTo(targetPixels);
Parallel.For(
minY,
maxY,
this.ParallelOptions,
y =>
{
for (int x = startX; x < endX; x++)
{
for (int x = startX; x < endX; x++)
int maxIntensity = 0;
int maxIndex = 0;
int[] intensityBin = new int[levels];
float[] redBin = new float[levels];
float[] blueBin = new float[levels];
float[] greenBin = new float[levels];
for (int fy = 0; fy <= radius; fy++)
{
int maxIntensity = 0;
int maxIndex = 0;
int fyr = fy - radius;
int offsetY = y + fyr;
int[] intensityBin = new int[levels];
float[] redBin = new float[levels];
float[] blueBin = new float[levels];
float[] greenBin = new float[levels];
// Skip the current row
if (offsetY < minY)
{
continue;
}
for (int fy = 0; fy <= radius; fy++)
// Outwith the current bounds so break.
if (offsetY >= maxY)
{
int fyr = fy - radius;
int offsetY = y + fyr;
break;
}
// Skip the current row
if (offsetY < minY)
{
continue;
}
for (int fx = 0; fx <= radius; fx++)
{
int fxr = fx - radius;
int offsetX = x + fxr;
// Outwith the current bounds so break.
if (offsetY >= maxY)
// Skip the column
if (offsetX < 0)
{
break;
continue;
}
for (int fx = 0; fx <= radius; fx++)
if (offsetX < maxX)
{
int fxr = fx - radius;
int offsetX = x + fxr;
// ReSharper disable once AccessToDisposedClosure
Vector4 color = sourcePixels[offsetX, offsetY].ToVector4();
// Skip the column
if (offsetX < 0)
{
continue;
}
if (offsetX < maxX)
{
// ReSharper disable once AccessToDisposedClosure
Vector4 color = sourcePixels[offsetX, offsetY].ToVector4();
float sourceRed = color.X;
float sourceBlue = color.Z;
float sourceGreen = color.Y;
float sourceRed = color.X;
float sourceBlue = color.Z;
float sourceGreen = color.Y;
int currentIntensity = (int)Math.Round((sourceBlue + sourceGreen + sourceRed) / 3.0 * (levels - 1));
int currentIntensity = (int)Math.Round((sourceBlue + sourceGreen + sourceRed) / 3.0 * (levels - 1));
intensityBin[currentIntensity] += 1;
blueBin[currentIntensity] += sourceBlue;
greenBin[currentIntensity] += sourceGreen;
redBin[currentIntensity] += sourceRed;
intensityBin[currentIntensity] += 1;
blueBin[currentIntensity] += sourceBlue;
greenBin[currentIntensity] += sourceGreen;
redBin[currentIntensity] += sourceRed;
if (intensityBin[currentIntensity] > maxIntensity)
{
maxIntensity = intensityBin[currentIntensity];
maxIndex = currentIntensity;
}
if (intensityBin[currentIntensity] > maxIntensity)
{
maxIntensity = intensityBin[currentIntensity];
maxIndex = currentIntensity;
}
}
}
float red = MathF.Abs(redBin[maxIndex] / maxIntensity);
float green = MathF.Abs(greenBin[maxIndex] / maxIntensity);
float blue = MathF.Abs(blueBin[maxIndex] / maxIntensity);
float red = MathF.Abs(redBin[maxIndex] / maxIntensity);
float green = MathF.Abs(greenBin[maxIndex] / maxIntensity);
float blue = MathF.Abs(blueBin[maxIndex] / maxIntensity);
TPixel packed = default(TPixel);
packed.PackFromVector4(new Vector4(red, green, blue, sourcePixels[x, y].ToVector4().W));
targetPixels[x, y] = packed;
}
TPixel packed = default(TPixel);
packed.PackFromVector4(new Vector4(red, green, blue, sourcePixels[x, y].ToVector4().W));
targetPixels[x, y] = packed;
}
});
}
}
});
source.SwapPixelsBuffers(targetPixels);
}

65
src/ImageSharp/Processing/Processors/Effects/PixelateProcessor.cs

@ -66,51 +66,46 @@ namespace ImageSharp.Processing.Processors
// Get the range on the y-plane to choose from.
IEnumerable<int> range = EnumerableExtensions.SteppedRange(minY, i => i < maxY, size);
using (PixelAccessor<TPixel> targetPixels = new PixelAccessor<TPixel>(source.Width, source.Height))
using (PixelAccessor<TPixel> sourcePixels = source.Lock())
{
using (PixelAccessor<TPixel> sourcePixels = source.Lock())
{
Parallel.ForEach(
range,
this.ParallelOptions,
y =>
Parallel.ForEach(
range,
this.ParallelOptions,
y =>
{
int offsetY = y - startY;
int offsetPy = offset;
for (int x = minX; x < maxX; x += size)
{
int offsetY = y - startY;
int offsetPy = offset;
int offsetX = x - startX;
int offsetPx = offset;
for (int x = minX; x < maxX; x += size)
// Make sure that the offset is within the boundary of the image.
while (offsetY + offsetPy >= maxY)
{
int offsetX = x - startX;
int offsetPx = offset;
// Make sure that the offset is within the boundary of the image.
while (offsetY + offsetPy >= maxY)
{
offsetPy--;
}
offsetPy--;
}
while (x + offsetPx >= maxX)
{
offsetPx--;
}
while (x + offsetPx >= maxX)
{
offsetPx--;
}
// Get the pixel color in the centre of the soon to be pixelated area.
// ReSharper disable AccessToDisposedClosure
TPixel pixel = sourcePixels[offsetX + offsetPx, offsetY + offsetPy];
// Get the pixel color in the centre of the soon to be pixelated area.
// ReSharper disable AccessToDisposedClosure
TPixel pixel = sourcePixels[offsetX + offsetPx, offsetY + offsetPy];
// For each pixel in the pixelate size, set it to the centre color.
for (int l = offsetY; l < offsetY + size && l < maxY; l++)
// For each pixel in the pixelate size, set it to the centre color.
for (int l = offsetY; l < offsetY + size && l < maxY; l++)
{
for (int k = offsetX; k < offsetX + size && k < maxX; k++)
{
for (int k = offsetX; k < offsetX + size && k < maxX; k++)
{
targetPixels[k, l] = pixel;
}
sourcePixels[k, l] = pixel;
}
}
});
source.SwapPixelsBuffers(targetPixels);
}
}
});
}
}
}

19
src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.Weights.cs

@ -52,13 +52,14 @@ namespace ImageSharp.Processing.Processors
/// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this <see cref="WeightsWindow"/> 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(MethodImplOptions.AggressiveInlining)]
public Vector4 ComputeWeightedRowSum(BufferSpan<Vector4> rowSpan)
public Vector4 ComputeWeightedRowSum(BufferSpan<Vector4> rowSpan, int sourceX)
{
ref float horizontalValues = ref this.Ptr;
int left = this.Left;
ref Vector4 vecPtr = ref Unsafe.Add(ref rowSpan.DangerousGetPinnableReference(), left);
ref Vector4 vecPtr = ref Unsafe.Add(ref rowSpan.DangerousGetPinnableReference(), left + sourceX);
// Destination color components
Vector4 result = Vector4.Zero;
@ -78,13 +79,14 @@ namespace ImageSharp.Processing.Processors
/// Applies <see cref="Vector4Extensions.Expand(float)"/> to all input vectors.
/// </summary>
/// <param name="rowSpan">The input span of vectors</param>
/// <param name="sourceX">The source row position.</param>
/// <returns>The weighted sum</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ComputeExpandedWeightedRowSum(BufferSpan<Vector4> rowSpan)
public Vector4 ComputeExpandedWeightedRowSum(BufferSpan<Vector4> rowSpan, int sourceX)
{
ref float horizontalValues = ref this.Ptr;
int left = this.Left;
ref Vector4 vecPtr = ref Unsafe.Add(ref rowSpan.DangerousGetPinnableReference(), left);
ref Vector4 vecPtr = ref Unsafe.Add(ref rowSpan.DangerousGetPinnableReference(), left + sourceX);
// Destination color components
Vector4 result = Vector4.Zero;
@ -100,14 +102,15 @@ namespace ImageSharp.Processing.Processors
}
/// <summary>
/// Computes the sum of vectors in 'firstPassPixels' at a column pointed by 'x',
/// Computes the sum of vectors in 'firstPassPixels' at a row pointed by 'x',
/// weighted by weight values, pointed by this <see cref="WeightsWindow"/> instance.
/// </summary>
/// <param name="firstPassPixels">The buffer of input vectors in row first order</param>
/// <param name="x">The column position</param>
/// <param name="x">The row position</param>
/// <param name="sourceY">The source column position.</param>
/// <returns>The weighted sum</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ComputeWeightedColumnSum(Buffer2D<Vector4> firstPassPixels, int x)
public Vector4 ComputeWeightedColumnSum(Buffer2D<Vector4> firstPassPixels, int x, int sourceY)
{
ref float verticalValues = ref this.Ptr;
int left = this.Left;
@ -118,7 +121,7 @@ namespace ImageSharp.Processing.Processors
for (int i = 0; i < this.Length; i++)
{
float yw = Unsafe.Add(ref verticalValues, i);
int index = left + i;
int index = left + i + sourceY;
result += firstPassPixels[x, index] * yw;
}

8
src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs

@ -133,7 +133,7 @@ namespace ImageSharp.Processing.Processors
for (int x = minX; x < maxX; x++)
{
WeightsWindow window = this.HorizontalWeights.Weights[x - startX];
firstPassPixels[x, y] = window.ComputeExpandedWeightedRowSum(tempRowBuffer);
firstPassPixels[x, y] = window.ComputeExpandedWeightedRowSum(tempRowBuffer, sourceX);
}
}
else
@ -141,7 +141,7 @@ namespace ImageSharp.Processing.Processors
for (int x = minX; x < maxX; x++)
{
WeightsWindow window = this.HorizontalWeights.Weights[x - startX];
firstPassPixels[x, y] = window.ComputeWeightedRowSum(tempRowBuffer);
firstPassPixels[x, y] = window.ComputeWeightedRowSum(tempRowBuffer, sourceX);
}
}
}
@ -162,7 +162,7 @@ namespace ImageSharp.Processing.Processors
for (int x = 0; x < width; x++)
{
// Destination color components
Vector4 destination = window.ComputeWeightedColumnSum(firstPassPixels, x);
Vector4 destination = window.ComputeWeightedColumnSum(firstPassPixels, x, sourceY);
destination = destination.Compress();
TPixel d = default(TPixel);
d.PackFromVector4(destination);
@ -174,7 +174,7 @@ namespace ImageSharp.Processing.Processors
for (int x = 0; x < width; x++)
{
// Destination color components
Vector4 destination = window.ComputeWeightedColumnSum(firstPassPixels, x);
Vector4 destination = window.ComputeWeightedColumnSum(firstPassPixels, x, sourceY);
TPixel d = default(TPixel);
d.PackFromVector4(destination);

4
tests/ImageSharp.Tests/Colors/ColorTests.cs → tests/ImageSharp.Tests/Colors/Rgba32Tests.cs

@ -1,4 +1,4 @@
// <copyright file="ColorTests.cs" company="James Jackson-South">
// <copyright file="Rgba32Tests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
@ -15,7 +15,7 @@ namespace ImageSharp.Tests
/// <summary>
/// Tests the <see cref="Rgba32"/> struct.
/// </summary>
public class ColorTests
public class Rgba32Tests
{
/// <summary>
/// Tests the equality operators for equality.

0
tests/ImageSharp.Tests/Colors/ColorTransformTests.cs → tests/ImageSharp.Tests/Colors/Rgba32TransformTests.cs

4
tests/ImageSharp.Tests/Colors/ColorVectorTests.cs → tests/ImageSharp.Tests/Colors/RgbaVectorTests.cs

@ -1,4 +1,4 @@
// <copyright file="ColorVectorTests.cs" company="James Jackson-South">
// <copyright file="RgbaVectorTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
@ -15,7 +15,7 @@ namespace ImageSharp.Tests
/// <summary>
/// Tests the <see cref="RgbaVector"/> struct.
/// </summary>
public class ColorVectorTests
public class RgbaVectorTests
{
/// <summary>
/// Tests the equality operators for equality.

43
tests/ImageSharp.Tests/Colors/ColorVectorTransformTests.cs → tests/ImageSharp.Tests/Colors/RgbaVectorTransformTests.cs

@ -1,4 +1,4 @@
// <copyright file="ColorVectorTransformTests.cs" company="James Jackson-South">
// <copyright file="RgbaVectorTransformTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
@ -12,7 +12,7 @@ namespace ImageSharp.Tests.Colors
/// Tests the color transform algorithms. Test results match the output of CSS equivalents.
/// <see href="https://jsfiddle.net/jamessouth/L1v8r6kh/"/>
/// </summary>
public class ColorVectorTransformTests
public class RgbaVectorTransformTests
{
private static readonly ApproximateFloatComparer FloatComparer = new ApproximateFloatComparer(0.01F);
@ -33,25 +33,26 @@ namespace ImageSharp.Tests.Colors
Assert.True(normal == Source);
}
[Fact]
public void Multiply()
{
Assert.Equal(RgbaVector.Multiply(Backdrop, RgbaVector.Black).ToVector4(), Rgba32.Black.ToVector4(), FloatComparer);
Assert.Equal(RgbaVector.Multiply(Backdrop, RgbaVector.White).ToVector4(), Backdrop.ToVector4(), FloatComparer);
RgbaVector multiply = RgbaVector.Multiply(Backdrop, Source);
Assert.Equal(multiply.ToVector4(), new RgbaVector(0, 41, 0).ToVector4(), FloatComparer);
}
[Fact]
public void Screen()
{
Assert.Equal(RgbaVector.Screen(Backdrop, RgbaVector.Black).ToVector4(), Backdrop.ToVector4(), FloatComparer);
Assert.Equal(RgbaVector.Screen(Backdrop, RgbaVector.White).ToVector4(), RgbaVector.White.ToVector4(), FloatComparer);
RgbaVector screen = RgbaVector.Screen(Backdrop, Source);
Assert.Equal(screen.ToVector4(), new RgbaVector(204, 163, 153).ToVector4(), FloatComparer);
}
// TODO: These tests keep sporadically breaking our builds. Fins out why they work locally but not on the CI.
// [Fact]
// public void Multiply()
// {
// Assert.Equal(RgbaVector.Multiply(Backdrop, RgbaVector.Black).ToVector4(), RgbaVector.Black.ToVector4(), FloatComparer);
// Assert.Equal(RgbaVector.Multiply(Backdrop, RgbaVector.White).ToVector4(), Backdrop.ToVector4(), FloatComparer);
// RgbaVector multiply = RgbaVector.Multiply(Backdrop, Source);
// Assert.Equal(multiply.ToVector4(), new RgbaVector(0, 41, 0).ToVector4(), FloatComparer);
// }
// [Fact]
// public void Screen()
// {
// Assert.Equal(RgbaVector.Screen(Backdrop, RgbaVector.Black).ToVector4(), Backdrop.ToVector4(), FloatComparer);
// Assert.Equal(RgbaVector.Screen(Backdrop, RgbaVector.White).ToVector4(), RgbaVector.White.ToVector4(), FloatComparer);
// RgbaVector screen = RgbaVector.Screen(Backdrop, Source);
// Assert.Equal(screen.ToVector4(), new RgbaVector(204, 163, 153).ToVector4(), FloatComparer);
// }
[Fact]
public void HardLight()

1
tests/ImageSharp.Tests/FileTestBase.cs

@ -30,6 +30,7 @@ namespace ImageSharp.Tests
TestFile.Create(TestImages.Bmp.Car),
// TestFile.Create(TestImages.Bmp.NegHeight), // Perf: Enable for local testing only
TestFile.Create(TestImages.Png.Splash),
// TestFile.Create(TestImages.Png.Cross), // Perf: Enable for local testing only
// TestFile.Create(TestImages.Png.ChunkLength1), // Perf: Enable for local testing only
// TestFile.Create(TestImages.Png.ChunkLength2), // Perf: Enable for local testing only
// TestFile.Create(TestImages.Png.Powerpoint), // Perf: Enable for local testing only

18
tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs

@ -3,15 +3,33 @@
// Licensed under the Apache License, Version 2.0.
// </copyright>
// ReSharper disable InconsistentNaming
namespace ImageSharp.Tests
{
using System.Text;
using Xunit;
using ImageSharp.Formats;
using ImageSharp.PixelFormats;
public class GifDecoderTests
{
private const PixelTypes PixelTypes = Tests.PixelTypes.StandardImageClass | Tests.PixelTypes.RgbaVector | Tests.PixelTypes.Argb32;
public static readonly string[] TestFiles = { TestImages.Gif.Giphy, TestImages.Gif.Rings, TestImages.Gif.Trans };
[Theory]
[WithFileCollection(nameof(TestFiles), PixelTypes)]
public void DecodeAndReSave<TPixel>(TestImageProvider<TPixel> imageProvider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = imageProvider.GetImage())
{
imageProvider.Utility.SaveTestOutputFile(image, "bmp");
imageProvider.Utility.SaveTestOutputFile(image, "gif");
}
}
[Fact]
public void Decode_IgnoreMetadataIsFalse_CommentsAreRead()
{

14
tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs

@ -9,9 +9,23 @@ namespace ImageSharp.Tests
using Xunit;
using ImageSharp.Formats;
using ImageSharp.PixelFormats;
public class GifEncoderTests
{
private const PixelTypes PixelTypes = Tests.PixelTypes.StandardImageClass | Tests.PixelTypes.RgbaVector | Tests.PixelTypes.Argb32;
[Theory]
[WithTestPatternImages(100, 100, PixelTypes)]
public void EncodeGeneratedPatterns<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
provider.Utility.SaveTestOutputFile(image, "gif", new GifEncoder());
}
}
[Fact]
public void Encode_IgnoreMetadataIsFalse_CommentsAreWritten()
{

20
tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs

@ -9,9 +9,29 @@ namespace ImageSharp.Tests
using Xunit;
using ImageSharp.Formats;
using ImageSharp.PixelFormats;
public class PngDecoderTests
{
private const PixelTypes PixelTypes = Tests.PixelTypes.StandardImageClass | Tests.PixelTypes.RgbaVector | Tests.PixelTypes.Argb32;
public static readonly string[] TestFiles =
{
TestImages.Png.Splash, TestImages.Png.Indexed, TestImages.Png.Interlaced, TestImages.Png.FilterVar,
TestImages.Png.ChunkLength1, TestImages.Png.ChunkLength2
};
[Theory]
[WithFileCollection(nameof(TestFiles), PixelTypes)]
public void DecodeAndReSave<TPixel>(TestImageProvider<TPixel> imageProvider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = imageProvider.GetImage())
{
imageProvider.Utility.SaveTestOutputFile(image, "bmp");
}
}
[Fact]
public void Decode_IgnoreMetadataIsFalse_TextChunckIsRead()
{

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

@ -7,6 +7,7 @@ using ImageSharp.Formats;
namespace ImageSharp.Tests
{
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@ -17,6 +18,29 @@ namespace ImageSharp.Tests
public class PngEncoderTests : FileTestBase
{
private const PixelTypes PixelTypes = Tests.PixelTypes.StandardImageClass | Tests.PixelTypes.RgbaVector | Tests.PixelTypes.Argb32;
[Theory]
[WithTestPatternImages(100, 100, PixelTypes, PngColorType.RgbWithAlpha)]
[WithTestPatternImages(100, 100, PixelTypes, PngColorType.Rgb)]
[WithTestPatternImages(100, 100, PixelTypes, PngColorType.Palette)]
[WithTestPatternImages(100, 100, PixelTypes, PngColorType.Grayscale)]
[WithTestPatternImages(100, 100, PixelTypes, PngColorType.GrayscaleWithAlpha)]
public void EncodeGeneratedPatterns<TPixel>(TestImageProvider<TPixel> provider, PngColorType pngColorType)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
PngEncoderOptions options = new PngEncoderOptions()
{
PngColorType = pngColorType
};
provider.Utility.TestName += "_" + pngColorType;
provider.Utility.SaveTestOutputFile(image, "png", new PngEncoder(), options);
}
}
[Theory]
[WithBlankImages(1, 1, PixelTypes.All)]
public void WritesFileMarker<TPixel>(TestImageProvider<TPixel> provider)

8
tests/ImageSharp.Tests/Formats/Png/PngSmokeTests.cs

@ -20,7 +20,7 @@ namespace ImageSharp.Tests.Formats.Png
public class PngSmokeTests
{
[Theory]
[WithTestPatternImages(300, 300, PixelTypes.All)]
[WithTestPatternImages(300, 300, PixelTypes.StandardImageClass)]
public void GeneralTest<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
@ -41,7 +41,7 @@ namespace ImageSharp.Tests.Formats.Png
}
[Theory]
[WithTestPatternImages(100, 100, PixelTypes.All)]
[WithTestPatternImages(100, 100, PixelTypes.StandardImageClass)]
public void CanSaveIndexedPng<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
@ -56,7 +56,7 @@ namespace ImageSharp.Tests.Formats.Png
using (Image img2 = Image.Load(ms, new PngDecoder()))
{
// img2.Save(provider.Utility.GetTestOutputFileName("bmp", "_loaded"), new BmpEncoder());
ImageComparer.CheckSimilarity(image, img2);
ImageComparer.CheckSimilarity(image, img2, 0.03f);
}
}
}
@ -105,7 +105,7 @@ namespace ImageSharp.Tests.Formats.Png
//}
[Theory]
[WithTestPatternImages(300, 300, PixelTypes.All)]
[WithTestPatternImages(300, 300, PixelTypes.StandardImageClass)]
public void Resize<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{

5
tests/ImageSharp.Tests/ImageComparer.cs

@ -73,7 +73,7 @@
if (b > segmentThreshold) { diffPixels++; }
}
return diffPixels / (scalingFactor * scalingFactor);
return (float)diffPixels / (float)(scalingFactor * scalingFactor);
}
private static Fast2DArray<byte> GetDifferences<TPixelA, TPixelB>(Image<TPixelA> source, Image<TPixelB> target, int scalingFactor)
@ -88,7 +88,8 @@
{
for (int x = 0; x < scalingFactor; x++)
{
differences[x, y] = (byte)Math.Abs(firstGray[x, y] - secondGray[x, y]);
var diff = firstGray[x, y] - secondGray[x, y];
differences[x, y] = (byte)Math.Abs(diff);
}
}

2
tests/ImageSharp.Tests/Processors/Filters/AlphaTest.cs

@ -43,7 +43,7 @@ namespace ImageSharp.Tests
foreach (TestFile file in Files)
{
string filename = file.GetFileName(value);
string filename = file.GetFileName(value + "-InBox");
using (Image image = file.CreateImage())
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{

16
tests/ImageSharp.Tests/Processors/Filters/BackgroundColorTest.cs

@ -27,5 +27,21 @@ namespace ImageSharp.Tests
}
}
}
[Fact]
public void ImageShouldApplyBackgroundColorFilterInBox()
{
string path = this.CreateOutputDirectory("BackgroundColor");
foreach (TestFile file in Files)
{
string filename = file.GetFileName("-InBox");
using (Image image = file.CreateImage())
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.BackgroundColor(Rgba32.HotPink, new Rectangle(10, 10, image.Width / 2, image.Height / 2)).Save(output);
}
}
}
}
}

17
tests/ImageSharp.Tests/Processors/Filters/BinaryThreshold.cs → tests/ImageSharp.Tests/Processors/Filters/BinaryThresholdTest.cs

@ -34,5 +34,22 @@ namespace ImageSharp.Tests
}
}
}
[Theory]
[MemberData(nameof(BinaryThresholdValues))]
public void ImageShouldApplyBinaryThresholdInBox(float value)
{
string path = this.CreateOutputDirectory("BinaryThreshold");
foreach (TestFile file in Files)
{
string filename = file.GetFileName(value + "-InBox");
using (Image image = file.CreateImage())
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.BinaryThreshold(value, new Rectangle(10, 10, image.Width / 2, image.Height / 2)).Save(output);
}
}
}
}
}

16
tests/ImageSharp.Tests/Processors/Filters/BlackWhiteTest.cs

@ -25,5 +25,21 @@ namespace ImageSharp.Tests
}
}
}
[Fact]
public void ImageShouldApplyBlackWhiteFilterInBox()
{
string path = this.CreateOutputDirectory("BlackWhite");
foreach (TestFile file in Files)
{
string filename = file.GetFileName("-InBox");
using (Image image = file.CreateImage())
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.BlackWhite(new Rectangle(10, 10, image.Width / 2, image.Height / 2)).Save(output);
}
}
}
}
}

17
tests/ImageSharp.Tests/Processors/Filters/BoxBlurTest.cs

@ -34,5 +34,22 @@ namespace ImageSharp.Tests
}
}
}
[Theory]
[MemberData(nameof(BoxBlurValues))]
public void ImageShouldApplyBoxBlurFilterInBox(int value)
{
string path = this.CreateOutputDirectory("BoxBlur");
foreach (TestFile file in Files)
{
string filename = file.GetFileName(value + "-InBox");
using (Image image = file.CreateImage())
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.BoxBlur(value, new Rectangle(10, 10, image.Width / 2, image.Height / 2)).Save(output);
}
}
}
}
}

17
tests/ImageSharp.Tests/Processors/Filters/BrightnessTest.cs

@ -34,5 +34,22 @@ namespace ImageSharp.Tests
}
}
}
[Theory]
[MemberData(nameof(BrightnessValues))]
public void ImageShouldApplyBrightnessFilterInBox(int value)
{
string path = this.CreateOutputDirectory("Brightness");
foreach (TestFile file in Files)
{
string filename = file.GetFileName(value + "-InBox");
using (Image image = file.CreateImage())
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.Brightness(value, new Rectangle(10, 10, image.Width / 2, image.Height / 2)).Save(output);
}
}
}
}
}

17
tests/ImageSharp.Tests/Processors/Filters/ColorBlindnessTest.cs

@ -41,5 +41,22 @@ namespace ImageSharp.Tests
}
}
}
[Theory]
[MemberData(nameof(ColorBlindnessFilters))]
public void ImageShouldApplyBrightnessFilterInBox(ColorBlindness colorBlindness)
{
string path = this.CreateOutputDirectory("ColorBlindness");
foreach (TestFile file in Files)
{
string filename = file.GetFileName(colorBlindness + "-InBox");
using (Image image = file.CreateImage())
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.ColorBlindness(colorBlindness, new Rectangle(10, 10, image.Width / 2, image.Height / 2)).Save(output);
}
}
}
}
}

17
tests/ImageSharp.Tests/Processors/Filters/ContrastTest.cs

@ -33,5 +33,22 @@ namespace ImageSharp.Tests
}
}
}
[Theory]
[MemberData(nameof(ContrastValues))]
public void ImageShouldApplyContrastFilterInBox(int value)
{
string path = this.CreateOutputDirectory("Contrast");
foreach (TestFile file in Files)
{
string filename = file.GetFileName(value + "-InBox");
using (Image image = file.CreateImage())
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.Contrast(value, new Rectangle(10, 10, image.Width / 2, image.Height / 2)).Save(output);
}
}
}
}
}

38
tests/ImageSharp.Tests/Processors/Filters/GaussianBlurTest.cs

@ -6,10 +6,10 @@
namespace ImageSharp.Tests
{
using System.IO;
using ImageSharp.PixelFormats;
using Xunit;
public class GaussianBlurTest : FileTestBase
public class GaussianBlurTest
{
public static readonly TheoryData<int> GaussianBlurValues
= new TheoryData<int>
@ -19,19 +19,33 @@ namespace ImageSharp.Tests
};
[Theory]
[MemberData(nameof(GaussianBlurValues))]
public void ImageShouldApplyGaussianBlurFilter(int value)
[WithTestPatternImages(nameof(GaussianBlurValues), 320, 240, PixelTypes.StandardImageClass)]
public void ImageShouldApplyGaussianBlurFilter<TPixel>(TestImageProvider<TPixel> provider, int value)
where TPixel : struct, IPixel<TPixel>
{
string path = this.CreateOutputDirectory("GaussianBlur");
using (Image<TPixel> image = provider.GetImage())
{
image.GaussianBlur(value)
.DebugSave(provider, value.ToString());
}
}
foreach (TestFile file in Files)
[Theory]
[WithTestPatternImages(nameof(GaussianBlurValues), 320, 240, PixelTypes.StandardImageClass)]
public void ImageShouldApplyGaussianBlurFilterInBox<TPixel>(TestImageProvider<TPixel> provider, int value)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> source = provider.GetImage())
using (Image<TPixel> image = new Image<TPixel>(source))
{
string filename = file.GetFileName(value);
using (Image image = file.CreateImage())
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.GaussianBlur(value).Save(output);
}
Rectangle rect = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2);
image.GaussianBlur(value, rect)
.DebugSave(provider, value.ToString());
// lets draw identical shapes over the blured areas and ensure that it didn't change the outer area
image.Fill(NamedColors<TPixel>.HotPink, rect);
source.Fill(NamedColors<TPixel>.HotPink, rect);
ImageComparer.CheckSimilarity(image, source);
}
}
}

36
tests/ImageSharp.Tests/Processors/Filters/GaussianSharpenTest.cs

@ -6,7 +6,7 @@
namespace ImageSharp.Tests
{
using System.IO;
using ImageSharp.PixelFormats;
using Xunit;
public class GaussianSharpenTest : FileTestBase
@ -19,19 +19,33 @@ namespace ImageSharp.Tests
};
[Theory]
[MemberData(nameof(GaussianSharpenValues))]
public void ImageShouldApplyGaussianSharpenFilter(int value)
[WithTestPatternImages(nameof(GaussianSharpenValues), 320, 240, PixelTypes.StandardImageClass)]
public void ImageShouldApplyGaussianSharpenFilter<TPixel>(TestImageProvider<TPixel> provider, int value)
where TPixel : struct, IPixel<TPixel>
{
string path = this.CreateOutputDirectory("GaussianSharpen");
using (Image<TPixel> image = provider.GetImage())
{
image.GaussianSharpen(value)
.DebugSave(provider, value.ToString());
}
}
foreach (TestFile file in Files)
[Theory]
[WithTestPatternImages(nameof(GaussianSharpenValues), 320, 240, PixelTypes.StandardImageClass)]
public void ImageShouldApplyGaussianSharpenFilterInBox<TPixel>(TestImageProvider<TPixel> provider, int value)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> source = provider.GetImage())
using (Image<TPixel> image = new Image<TPixel>(source))
{
string filename = file.GetFileName(value);
using (Image image = file.CreateImage())
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.GaussianSharpen(value).Save(output);
}
Rectangle rect = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2);
image.GaussianSharpen(value, rect)
.DebugSave(provider, value.ToString());
// lets draw identical shapes over the Sharpened areas and ensure that it didn't change the outer area
image.Fill(NamedColors<TPixel>.HotPink, rect);
source.Fill(NamedColors<TPixel>.HotPink, rect);
ImageComparer.CheckSimilarity(image, source);
}
}
}

28
tests/ImageSharp.Tests/Processors/Filters/GrayscaleTest.cs

@ -5,12 +5,8 @@
namespace ImageSharp.Tests
{
using System.IO;
using Xunit;
using ImageSharp.Processing;
using ImageSharp.Tests;
using System.Numerics;
using ImageSharp.PixelFormats;
@ -20,7 +16,7 @@ namespace ImageSharp.Tests
/// Use test patterns over loaded images to save decode time.
/// </summary>
[Theory]
[WithTestPatternImages(50, 50, PixelTypes.StandardImageClass, GrayscaleMode.Bt709)]
[WithTestPatternImages(50, 50, PixelTypes.StandardImageClass, GrayscaleMode.Bt709)]
[WithTestPatternImages(50, 50, PixelTypes.StandardImageClass, GrayscaleMode.Bt601)]
public void ImageShouldApplyGrayscaleFilterAll<TPixel>(TestImageProvider<TPixel> provider, GrayscaleMode value)
where TPixel : struct, IPixel<TPixel>
@ -36,7 +32,27 @@ namespace ImageSharp.Tests
Assert.Equal(data[1], data[2]);
}
image.DebugSave(provider);
image.DebugSave(provider, value.ToString());
}
}
[Theory]
[WithTestPatternImages(50, 50, PixelTypes.StandardImageClass, GrayscaleMode.Bt709)]
[WithTestPatternImages(50, 50, PixelTypes.StandardImageClass, GrayscaleMode.Bt601)]
public void ImageShouldApplyGrayscaleFilterInBox<TPixel>(TestImageProvider<TPixel> provider, GrayscaleMode value)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> source = provider.GetImage())
using (Image<TPixel> image = new Image<TPixel>(source))
{
Rectangle rect = new Rectangle(image.Width / 4, image.Height / 4, image.Width / 2, image.Height / 2);
image.Grayscale(rect, value)
.DebugSave(provider, value.ToString());
// Let's draw identical shapes over the greyed areas and ensure that it didn't change the outer area
image.Fill(NamedColors<TPixel>.HotPink, rect);
source.Fill(NamedColors<TPixel>.HotPink, rect);
ImageComparer.CheckSimilarity(image, source);
}
}
}

17
tests/ImageSharp.Tests/Processors/Filters/HueTest.cs

@ -34,5 +34,22 @@ namespace ImageSharp.Tests
}
}
}
[Theory]
[MemberData(nameof(HueValues))]
public void ImageShouldApplyHueFilterInBox(int value)
{
string path = this.CreateOutputDirectory("Hue");
foreach (TestFile file in Files)
{
string filename = file.GetFileName(value + "-InBox");
using (Image image = file.CreateImage())
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.Hue(value, new Rectangle(10, 10, image.Width / 2, image.Height / 2)).Save(output);
}
}
}
}
}

16
tests/ImageSharp.Tests/Processors/Filters/KodachromeTest.cs

@ -25,5 +25,21 @@ namespace ImageSharp.Tests
}
}
}
[Fact]
public void ImageShouldApplyKodachromeFilterInBox()
{
string path = this.CreateOutputDirectory("Kodachrome");
foreach (TestFile file in Files)
{
string filename = file.GetFileName("InBox");
using (Image image = file.CreateImage())
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.Kodachrome(new Rectangle(10, 10, image.Width / 2, image.Height / 2)).Save(output);
}
}
}
}
}

79
tests/ImageSharp.Tests/Processors/Filters/PixelateTest.cs

@ -6,10 +6,10 @@
namespace ImageSharp.Tests
{
using System.IO;
using ImageSharp.PixelFormats;
using Xunit;
public class PixelateTest : FileTestBase
public class PixelateTest
{
public static readonly TheoryData<int> PixelateValues
= new TheoryData<int>
@ -19,37 +19,74 @@ namespace ImageSharp.Tests
};
[Theory]
[MemberData(nameof(PixelateValues))]
public void ImageShouldApplyPixelateFilter(int value)
[WithTestPatternImages(nameof(PixelateValues), 320, 240, PixelTypes.StandardImageClass)]
public void ImageShouldApplyPixelateFilter<TPixel>(TestImageProvider<TPixel> provider, int value)
where TPixel : struct, IPixel<TPixel>
{
string path = CreateOutputDirectory("Pixelate");
foreach (TestFile file in Files)
using (Image<TPixel> image = provider.GetImage())
{
string filename = file.GetFileName(value);
Image image = file.CreateImage();
image.Pixelate(value)
.DebugSave(provider, new
{
size = value
});
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
using (PixelAccessor<TPixel> pixels = image.Lock())
{
image.Pixelate(value)
.Save(output);
for (int y = 0; y < pixels.Height; y += value)
{
for (int x = 0; x < pixels.Width; x += value)
{
TPixel source = pixels[x, y];
for (int pixY = y; pixY < y + value && pixY < pixels.Height; pixY++)
{
for (int pixX = x; pixX < x + value && pixX < pixels.Width; pixX++)
{
Assert.Equal(source, pixels[pixX, pixY]);
}
}
}
}
}
}
}
[Theory]
[MemberData(nameof(PixelateValues))]
public void ImageShouldApplyPixelateFilterInBox(int value)
[WithTestPatternImages(nameof(PixelateValues), 320, 240, PixelTypes.StandardImageClass)]
public void ImageShouldApplyPixelateFilterInBox<TPixel>(TestImageProvider<TPixel> provider, int value)
where TPixel : struct, IPixel<TPixel>
{
string path = this.CreateOutputDirectory("Pixelate");
foreach (TestFile file in Files)
using (Image<TPixel> source = provider.GetImage())
using (Image<TPixel> image = new Image<TPixel>(source))
{
string filename = file.GetFileName(value + "-InBox");
using (Image image = file.CreateImage())
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
Rectangle rect = new Rectangle(image.Width/4, image.Height / 4, image.Width / 2, image.Height / 2);
image.Pixelate(value, rect)
.DebugSave(provider, new
{
size = value
});
using (PixelAccessor<TPixel> pixels = image.Lock())
using (PixelAccessor<TPixel> sourcePixels = source.Lock())
{
image.Pixelate(value, new Rectangle(10, 10, image.Width / 2, image.Height / 2)).Save(output);
for (int y = 0; y < pixels.Height; y++)
{
for (int x = 0; x < pixels.Width; x++)
{
var tx = x;
var ty = y;
TPixel sourceColor = sourcePixels[tx, ty];
if (rect.Contains(tx, ty))
{
var sourceX = tx - ((tx - rect.Left) % value) + (value / 2);
var sourceY = ty - ((ty - rect.Top) % value) + (value / 2);
sourceColor = pixels[sourceX, sourceY];
}
Assert.Equal(sourceColor, pixels[tx, ty]);
}
}
}
}
}

16
tests/ImageSharp.Tests/Processors/Filters/PolaroidTest.cs

@ -25,5 +25,21 @@ namespace ImageSharp.Tests
}
}
}
[Fact]
public void ImageShouldApplyPolaroidFilterInBox()
{
string path = this.CreateOutputDirectory("Polaroid");
foreach (TestFile file in Files)
{
string filename = file.GetFileName("InBox");
using (Image image = file.CreateImage())
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.Polaroid(new Rectangle(10, 10, image.Width / 2, image.Height / 2)).Save(output);
}
}
}
}
}

17
tests/ImageSharp.Tests/Processors/Filters/SaturationTest.cs

@ -34,5 +34,22 @@ namespace ImageSharp.Tests
}
}
}
[Theory]
[MemberData(nameof(SaturationValues))]
public void ImageShouldApplySaturationFilterInBox(int value)
{
string path = this.CreateOutputDirectory("Saturation");
foreach (TestFile file in Files)
{
string filename = file.GetFileName(value + "-InBox");
using (Image image = file.CreateImage())
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.Saturation(value, new Rectangle(10, 10, image.Width / 2, image.Height / 2)).Save(output);
}
}
}
}
}

16
tests/ImageSharp.Tests/Processors/Filters/SepiaTest.cs

@ -25,5 +25,21 @@ namespace ImageSharp.Tests
}
}
}
[Fact]
public void ImageShouldApplySepiaFilterInBox()
{
string path = this.CreateOutputDirectory("Sepia");
foreach (TestFile file in Files)
{
string filename = file.GetFileName("InBox");
using (Image image = file.CreateImage())
using (FileStream output = File.OpenWrite($"{path}/{filename}"))
{
image.Sepia(new Rectangle(10, 10, image.Width / 2, image.Height / 2)).Save(output);
}
}
}
}
}

1
tests/ImageSharp.Tests/TestImages.cs

@ -21,6 +21,7 @@ namespace ImageSharp.Tests
public const string Blur = "Png/blur.png";
public const string Indexed = "Png/indexed.png";
public const string Splash = "Png/splash.png";
public const string Cross = "Png/cross.png";
public const string Powerpoint = "Png/pp.png";
public const string SplashInterlaced = "Png/splash-interlaced.png";
public const string Interlaced = "Png/interlaced.png";

3
tests/ImageSharp.Tests/TestImages/Formats/Png/cross.png

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

118
tests/ImageSharp.Tests/TestUtilities/Attributes/ImageDataAttributeBase.cs

@ -21,26 +21,72 @@ namespace ImageSharp.Tests
protected readonly PixelTypes PixelTypes;
protected ImageDataAttributeBase(PixelTypes pixelTypes, object[] additionalParameters)
protected ImageDataAttributeBase(string memberName, PixelTypes pixelTypes, object[] additionalParameters)
{
this.PixelTypes = pixelTypes;
this.AdditionalParameters = additionalParameters;
this.MemberName = memberName;
}
public string MemberName { get; private set; }
public Type MemberType { get; set; }
public override IEnumerable<object[]> GetData(MethodInfo testMethod)
{
TypeInfo type = testMethod.GetParameters().First().ParameterType.GetTypeInfo();
if (!type.IsGenericType || type.GetGenericTypeDefinition() != typeof(TestImageProvider<>))
IEnumerable<object[]> addedRows = Enumerable.Empty<object[]>();
if (!string.IsNullOrWhiteSpace(this.MemberName))
{
Type type = this.MemberType ?? testMethod.DeclaringType;
Func<object> accessor = GetPropertyAccessor(type) ?? GetFieldAccessor(type);// ?? GetMethodAccessor(type);
if (accessor != null)
{
object obj = accessor();
if (obj is IEnumerable<object> memberItems)
{
addedRows = memberItems.Select(x => x as object[]);
if (addedRows.Any(x => x == null))
{
throw new ArgumentException($"Property {MemberName} on {MemberType ?? testMethod.DeclaringType} yielded an item that is not an object[]");
}
}
}
}
if (!addedRows.Any())
{
yield return this.AdditionalParameters;
addedRows = new[] { new object[0] };
}
bool firstIsprovider = FirstIsProvider(testMethod);
IEnumerable<object[]> dataItems = Enumerable.Empty<object[]>();
if (firstIsprovider)
{
return InnerGetData(testMethod, addedRows);
}
else
{
foreach (KeyValuePair<PixelTypes, Type> kv in this.PixelTypes.ExpandAllTypes())
{
Type factoryType = typeof(TestImageProvider<>).MakeGenericType(kv.Value);
return addedRows.Select(x => x.Concat(this.AdditionalParameters).ToArray());
}
}
private bool FirstIsProvider(MethodInfo testMethod)
{
TypeInfo dataType = testMethod.GetParameters().First().ParameterType.GetTypeInfo();
return dataType.IsGenericType && dataType.GetGenericTypeDefinition() == typeof(TestImageProvider<>);
}
private IEnumerable<object[]> InnerGetData(MethodInfo testMethod, IEnumerable<object[]> memberData)
{
foreach (KeyValuePair<PixelTypes, Type> kv in this.PixelTypes.ExpandAllTypes())
{
Type factoryType = typeof(TestImageProvider<>).MakeGenericType(kv.Value);
foreach (object[] originalFacoryMethodArgs in this.GetAllFactoryMethodArgs(testMethod, factoryType))
foreach (object[] originalFacoryMethodArgs in this.GetAllFactoryMethodArgs(testMethod, factoryType))
{
foreach (object[] row in memberData)
{
object[] actualFactoryMethodArgs = new object[originalFacoryMethodArgs.Length + 2];
Array.Copy(originalFacoryMethodArgs, actualFactoryMethodArgs, originalFacoryMethodArgs.Length);
@ -50,9 +96,10 @@ namespace ImageSharp.Tests
object factory = factoryType.GetMethod(this.GetFactoryMethodName(testMethod))
.Invoke(null, actualFactoryMethodArgs);
object[] result = new object[this.AdditionalParameters.Length + 1];
object[] result = new object[this.AdditionalParameters.Length + 1 + row.Length];
result[0] = factory;
Array.Copy(this.AdditionalParameters, 0, result, 1, this.AdditionalParameters.Length);
Array.Copy(row, 0, result, 1, row.Length);
Array.Copy(this.AdditionalParameters, 0, result, 1 + row.Length, this.AdditionalParameters.Length);
yield return result;
}
}
@ -71,5 +118,56 @@ namespace ImageSharp.Tests
}
protected abstract string GetFactoryMethodName(MethodInfo testMethod);
Func<object> GetFieldAccessor(Type type)
{
FieldInfo fieldInfo = null;
for (Type reflectionType = type; reflectionType != null; reflectionType = reflectionType.GetTypeInfo().BaseType)
{
fieldInfo = reflectionType.GetRuntimeField(MemberName);
if (fieldInfo != null)
break;
}
if (fieldInfo == null || !fieldInfo.IsStatic)
return null;
return () => fieldInfo.GetValue(null);
}
//Func<object> GetMethodAccessor(Type type)
//{
// MethodInfo methodInfo = null;
// var parameterTypes = Parameters == null ? new Type[0] : Parameters.Select(p => p?.GetType()).ToArray();
// for (var reflectionType = type; reflectionType != null; reflectionType = reflectionType.GetTypeInfo().BaseType)
// {
// methodInfo = reflectionType.GetRuntimeMethods()
// .FirstOrDefault(m => m.Name == MemberName && ParameterTypesCompatible(m.GetParameters(), parameterTypes));
// if (methodInfo != null)
// break;
// }
// if (methodInfo == null || !methodInfo.IsStatic)
// return null;
// return () => methodInfo.Invoke(null, Parameters);
//}
Func<object> GetPropertyAccessor(Type type)
{
PropertyInfo propInfo = null;
for (Type reflectionType = type; reflectionType != null; reflectionType = reflectionType.GetTypeInfo().BaseType)
{
propInfo = reflectionType.GetRuntimeProperty(MemberName);
if (propInfo != null)
break;
}
if (propInfo == null || propInfo.GetMethod == null || !propInfo.GetMethod.IsStatic)
return null;
return () => propInfo.GetValue(null, null);
}
}
}

16
tests/ImageSharp.Tests/TestUtilities/Attributes/WithBlankImageAttribute.cs

@ -22,7 +22,21 @@ namespace ImageSharp.Tests
/// <param name="pixelTypes">The requested parameter</param>
/// <param name="additionalParameters">Additional theory parameter values</param>
public WithBlankImagesAttribute(int width, int height, PixelTypes pixelTypes, params object[] additionalParameters)
: base(pixelTypes, additionalParameters)
: base(null, pixelTypes, additionalParameters)
{
this.Width = width;
this.Height = height;
}
/// <summary>
/// Triggers passing an <see cref="TestImageProvider{TPixel}"/> that produces a blank image of size width * height
/// </summary>
/// <param name="width">The required width</param>
/// <param name="height">The required height</param>
/// <param name="pixelTypes">The requested parameter</param>
/// <param name="additionalParameters">Additional theory parameter values</param>
public WithBlankImagesAttribute(string memberData, int width, int height, PixelTypes pixelTypes, params object[] additionalParameters)
: base(memberData, pixelTypes, additionalParameters)
{
this.Width = width;
this.Height = height;

15
tests/ImageSharp.Tests/TestUtilities/Attributes/WithFileAttribute.cs

@ -24,7 +24,20 @@ namespace ImageSharp.Tests
/// <param name="pixelTypes">The requested pixel types</param>
/// <param name="additionalParameters">Additional theory parameter values</param>
public WithFileAttribute(string fileName, PixelTypes pixelTypes, params object[] additionalParameters)
: base(pixelTypes, additionalParameters)
: base(null, pixelTypes, additionalParameters)
{
this.fileName = fileName;
}
/// <summary>
/// Triggers passing <see cref="TestImageProvider{TPixel}"/> instances which read an image from the given file
/// One <see cref="TestImageProvider{TPixel}"/> instance will be passed for each the pixel format defined by the pixelTypes parameter
/// </summary>
/// <param name="fileName">The name of the file</param>
/// <param name="pixelTypes">The requested pixel types</param>
/// <param name="additionalParameters">Additional theory parameter values</param>
public WithFileAttribute(string fileName, string dataMemberName, PixelTypes pixelTypes, params object[] additionalParameters)
: base(dataMemberName, pixelTypes, additionalParameters)
{
this.fileName = fileName;
}

19
tests/ImageSharp.Tests/TestUtilities/Attributes/WithFileCollectionAttribute.cs

@ -29,7 +29,24 @@ namespace ImageSharp.Tests
string enumeratorMemberName,
PixelTypes pixelTypes,
params object[] additionalParameters)
: base(pixelTypes, additionalParameters)
: base(null, pixelTypes, additionalParameters)
{
this.enumeratorMemberName = enumeratorMemberName;
}
/// <summary>
/// Triggers passing <see cref="TestImageProvider{TPixel}"/> instances which read an image for each file being enumerated by the (static) test class field/property defined by enumeratorMemberName
/// <see cref="TestImageProvider{TPixel}"/> instances will be passed for each the pixel format defined by the pixelTypes parameter
/// </summary>
/// <param name="enumeratorMemberName">The name of the static test class field/property enumerating the files</param>
/// <param name="pixelTypes">The requested pixel types</param>
/// <param name="additionalParameters">Additional theory parameter values</param>
public WithFileCollectionAttribute(
string enumeratorMemberName,
string DataMemberName,
PixelTypes pixelTypes,
params object[] additionalParameters)
: base(DataMemberName, pixelTypes, additionalParameters)
{
this.enumeratorMemberName = enumeratorMemberName;
}

2
tests/ImageSharp.Tests/TestUtilities/Attributes/WithMemberFactoryAttribute.cs

@ -26,7 +26,7 @@ namespace ImageSharp.Tests
/// <param name="pixelTypes">The requested pixel types</param>
/// <param name="additionalParameters">Additional theory parameter values</param>
public WithMemberFactoryAttribute(string memberMethodName, PixelTypes pixelTypes, params object[] additionalParameters)
: base(pixelTypes, additionalParameters)
: base(null, pixelTypes, additionalParameters)
{
this.memberMethodName = memberMethodName;
}

14
tests/ImageSharp.Tests/TestUtilities/Attributes/WithTestPatternImageAttribute.cs

@ -22,7 +22,19 @@ namespace ImageSharp.Tests
/// <param name="pixelTypes">The requested parameter</param>
/// <param name="additionalParameters">Additional theory parameter values</param>
public WithTestPatternImagesAttribute(int width, int height, PixelTypes pixelTypes, params object[] additionalParameters)
: base(pixelTypes, additionalParameters)
: this(null, width, height, pixelTypes,additionalParameters)
{
}
/// <summary>
/// Triggers passing an <see cref="TestImageProvider{TPixel}"/> that produces a test pattern image of size width * height
/// </summary>
/// <param name="width">The required width</param>
/// <param name="height">The required height</param>
/// <param name="pixelTypes">The requested parameter</param>
/// <param name="additionalParameters">Additional theory parameter values</param>
public WithTestPatternImagesAttribute(string memberData, int width, int height, PixelTypes pixelTypes, params object[] additionalParameters)
: base(memberData, pixelTypes, additionalParameters)
{
this.Width = width;
this.Height = height;

8
tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs

@ -11,12 +11,16 @@ namespace ImageSharp.Tests
using ImageSharp.PixelFormats;
using Xunit.Abstractions;
public interface ITestImageProvider
{
PixelTypes PixelType { get; }
ImagingTestCaseUtility Utility { get; }
}
/// <summary>
/// Provides <see cref="Image{TPixel}" /> instances for parametric unit tests.
/// </summary>
/// <typeparam name="TPixel">The pixel format of the image</typeparam>
public abstract partial class TestImageProvider<TPixel>
public abstract partial class TestImageProvider<TPixel> : ITestImageProvider
where TPixel : struct, IPixel<TPixel>
{
public PixelTypes PixelType { get; private set; } = typeof(TPixel).GetPixelType();

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

@ -97,10 +97,10 @@ namespace ImageSharp.Tests
/// <param name="extension">The requested extension</param>
/// <param name="encoder">Optional encoder</param>
/// <param name="options">Optional encoder options</param>
public void SaveTestOutputFile<TPixel>(Image<TPixel> image, string extension = null, IImageEncoder encoder = null, IEncoderOptions options = null)
public void SaveTestOutputFile<TPixel>(Image<TPixel> image, string extension = null, IImageEncoder encoder = null, IEncoderOptions options = null, string tag = null)
where TPixel : struct, IPixel<TPixel>
{
string path = this.GetTestOutputFileName(extension);
string path = this.GetTestOutputFileName(extension: extension, tag:tag);
extension = Path.GetExtension(path);
IImageFormat format = GetImageFormatByExtension(extension);

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

@ -3,19 +3,32 @@ namespace ImageSharp.Tests
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using ImageSharp.PixelFormats;
public static class TestImageExtensions
{
public static void DebugSave<TPixel>(this Image<TPixel> img, TestImageProvider<TPixel> provider, string extension = "png")
public static void DebugSave<TPixel>(this Image<TPixel> img, ITestImageProvider provider, object settings = null, string extension = "png")
where TPixel : struct, IPixel<TPixel>
{
string tag = null;
if (settings is string)
{
tag = (string)settings;
}
else if (settings != null)
{
var properties = settings.GetType().GetRuntimeProperties();
tag = string.Join("_", properties.ToDictionary(x => x.Name, x => x.GetValue(settings)).Select(x => $"{x.Key}-{x.Value}"));
}
if(!bool.TryParse(Environment.GetEnvironmentVariable("CI"), out bool isCI) || !isCI)
{
// we are running locally then we want to save it out
provider.Utility.SaveTestOutputFile(img, extension);
provider.Utility.SaveTestOutputFile(img, extension, tag: tag);
}
}
}

Loading…
Cancel
Save