Browse Source

Merge remote-tracking branch 'origin/master' into webp

# Conflicts:
#	tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
pull/1552/head
Brian Popow 5 years ago
parent
commit
1e67e9b22e
  1. 22
      src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs
  2. 60
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb444TiffColor{TPixel}.cs
  3. 1
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs
  4. 44
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
  5. 20
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs
  6. 28
      src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs
  7. 20
      src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs
  8. 40
      src/ImageSharp/Formats/Tiff/TiffBitsPerSampleExtensions.cs
  9. 4
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  10. 31
      src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
  11. 9
      src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
  12. 11
      src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
  13. 33
      src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs
  14. 23
      src/ImageSharp/Image.cs
  15. 108
      src/ImageSharp/ImageFrameCollection.cs
  16. 88
      src/ImageSharp/ImageFrameCollection{TPixel}.cs
  17. 66
      src/ImageSharp/Image{TPixel}.cs
  18. 9
      src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor.cs
  19. 9
      src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs
  20. 44
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs
  21. 1
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs
  22. 76
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  23. 42
      tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
  24. 40
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  25. 36
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
  26. 5
      tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
  27. 42
      tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs
  28. 36
      tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs
  29. 11
      tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs
  30. 69
      tests/ImageSharp.Tests/Image/ImageTests.cs
  31. 39
      tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
  32. 10
      tests/ImageSharp.Tests/TestImages.cs
  33. 145
      tests/ImageSharp.Tests/TestUtilities/PausedStream.cs
  34. 9
      tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs
  35. 19
      tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs
  36. 3
      tests/Images/Input/Tiff/flower-minisblack-04.tiff
  37. 3
      tests/Images/Input/Tiff/flower-palette-04.tiff
  38. 3
      tests/Images/Input/Tiff/flower-rgb-contig-02.tiff
  39. 3
      tests/Images/Input/Tiff/flower-rgb-contig-04.tiff
  40. 3
      tests/Images/Input/Tiff/flower-rgb-contig-10.tiff
  41. 3
      tests/Images/Input/Tiff/flower-rgb-contig-14.tiff
  42. 3
      tests/Images/Input/Tiff/flower-rgb-planar-02.tiff
  43. 3
      tests/Images/Input/Tiff/flower-rgb-planar-04.tiff
  44. 3
      tests/Images/Input/Tiff/flower-rgb-planar-10.tiff
  45. 3
      tests/Images/Input/Tiff/flower-rgb-planar-14.tiff

22
src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs

@ -96,10 +96,30 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Constants
public static readonly ushort[] BitsPerSample8Bit = { 8 };
/// <summary>
/// The bits per sample for images with 8 bits for each color channel.
/// The bits per sample for color images with 2 bits for each color channel.
/// </summary>
public static readonly ushort[] BitsPerSampleRgb2Bit = { 2, 2, 2 };
/// <summary>
/// The bits per sample for color images with 4 bits for each color channel.
/// </summary>
public static readonly ushort[] BitsPerSampleRgb4Bit = { 4, 4, 4 };
/// <summary>
/// The bits per sample for color images with 8 bits for each color channel.
/// </summary>
public static readonly ushort[] BitsPerSampleRgb8Bit = { 8, 8, 8 };
/// <summary>
/// The bits per sample for color images with 10 bits for each color channel.
/// </summary>
public static readonly ushort[] BitsPerSampleRgb10Bit = { 10, 10, 10 };
/// <summary>
/// The bits per sample for color images with 14 bits for each color channel.
/// </summary>
public static readonly ushort[] BitsPerSampleRgb14Bit = { 14, 14, 14 };
/// <summary>
/// The list of mimetypes that equate to a tiff.
/// </summary>

60
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb444TiffColor{TPixel}.cs

@ -0,0 +1,60 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
{
/// <summary>
/// Implements the 'RGB' photometric interpretation for 4 bits per color channel images.
/// </summary>
internal class Rgb444TiffColor<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{
var color = default(TPixel);
int offset = 0;
var bgra = default(Bgra4444);
for (int y = top; y < top + height; y++)
{
Span<TPixel> pixelRow = pixels.GetRowSpan(y);
for (int x = left; x < left + width; x += 2)
{
byte r = (byte)((data[offset] & 0xF0) >> 4);
byte g = (byte)(data[offset] & 0xF);
offset++;
byte b = (byte)((data[offset] & 0xF0) >> 4);
bgra.PackedValue = ToBgraPackedValue(b, g, r);
color.FromScaledVector4(bgra.ToScaledVector4());
pixelRow[x] = color;
if (x + 1 >= pixelRow.Length)
{
offset++;
break;
}
r = (byte)(data[offset] & 0xF);
offset++;
g = (byte)((data[offset] & 0xF0) >> 4);
b = (byte)(data[offset] & 0xF);
offset++;
bgra.PackedValue = ToBgraPackedValue(b, g, r);
color.FromScaledVector4(bgra.ToScaledVector4());
pixelRow[x + 1] = color;
}
}
}
private static ushort ToBgraPackedValue(byte b, byte g, byte r) => (ushort)(b | (g << 4) | (r << 8) | (0xF << 12));
}
}

1
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs

@ -27,7 +27,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
private readonly ushort bitsPerSampleB;
public RgbPlanarTiffColor(ushort[] bitsPerSample)
/* : base(bitsPerSample, null) */
{
this.bitsPerSampleR = bitsPerSample[0];
this.bitsPerSampleG = bitsPerSample[1];

44
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs

@ -57,16 +57,56 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new RgbTiffColor<TPixel>(bitsPerSample);
case TiffColorType.Rgb222:
DebugGuard.IsTrue(
bitsPerSample.Length == 3
&& bitsPerSample[2] == 2
&& bitsPerSample[1] == 2
&& bitsPerSample[0] == 2,
"bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new RgbTiffColor<TPixel>(bitsPerSample);
case TiffColorType.Rgb444:
DebugGuard.IsTrue(
bitsPerSample.Length == 3
&& bitsPerSample[2] == 4
&& bitsPerSample[1] == 4
&& bitsPerSample[0] == 4,
"bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new Rgb444TiffColor<TPixel>();
case TiffColorType.Rgb888:
DebugGuard.IsTrue(
bitsPerSample.Length == 3
&& bitsPerSample[0] == 8
&& bitsPerSample[2] == 8
&& bitsPerSample[1] == 8
&& bitsPerSample[2] == 8,
&& bitsPerSample[0] == 8,
"bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new Rgb888TiffColor<TPixel>();
case TiffColorType.Rgb101010:
DebugGuard.IsTrue(
bitsPerSample.Length == 3
&& bitsPerSample[2] == 10
&& bitsPerSample[1] == 10
&& bitsPerSample[0] == 10,
"bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new RgbTiffColor<TPixel>(bitsPerSample);
case TiffColorType.Rgb141414:
DebugGuard.IsTrue(
bitsPerSample.Length == 3
&& bitsPerSample[2] == 14
&& bitsPerSample[1] == 14
&& bitsPerSample[0] == 14,
"bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
return new RgbTiffColor<TPixel>(bitsPerSample);
case TiffColorType.PaletteColor:
DebugGuard.NotNull(bitsPerSample, "bitsPerSample");
DebugGuard.NotNull(colorMap, "colorMap");

20
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs

@ -58,11 +58,31 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
/// </summary>
Rgb,
/// <summary>
/// RGB color image with 2 bits for each channel.
/// </summary>
Rgb222,
/// <summary>
/// RGB color image with 4 bits for each channel.
/// </summary>
Rgb444,
/// <summary>
/// RGB Full Color. Optimized implementation for 8-bit images.
/// </summary>
Rgb888,
/// <summary>
/// RGB color image with 10 bits for each channel.
/// </summary>
Rgb101010,
/// <summary>
/// RGB color image with 14 bits for each channel.
/// </summary>
Rgb141414,
/// <summary>
/// RGB Full Color. Planar configuration of data.
/// </summary>

28
src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs

@ -18,14 +18,42 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary>
Bit4 = 4,
/// <summary>
/// 6 bits per pixel. 2 bit for each color channel.
///
/// Note: The TiffEncoder does not yet support 2 bits per color channel and will default to 24 bits per pixel instead.
/// </summary>
Bit6 = 6,
/// <summary>
/// 8 bits per pixel, grayscale or color palette images.
/// </summary>
Bit8 = 8,
/// <summary>
/// 12 bits per pixel. 4 bit for each color channel.
///
/// Note: The TiffEncoder does not yet support 4 bits per color channel and will default to 24 bits per pixel instead.
/// </summary>
Bit12 = 12,
/// <summary>
/// 24 bits per pixel. One byte for each color channel.
/// </summary>
Bit24 = 24,
/// <summary>
/// 30 bits per pixel. 10 bit for each color channel.
///
/// Note: The TiffEncoder does not yet support 10 bits per color channel and will default to 24 bits per pixel instead.
/// </summary>
Bit30 = 30,
/// <summary>
/// 42 bits per pixel. 14 bit for each color channel.
///
/// Note: The TiffEncoder does not yet support 14 bits per color channel and will default to 24 bits per pixel instead.
/// </summary>
Bit42 = 42,
}
}

20
src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs

@ -28,9 +28,29 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary>
Bit8 = 8,
/// <summary>
/// Six bits per sample, each channel has 2 bits.
/// </summary>
Bit6 = 6,
/// <summary>
/// Twelve bits per sample, each channel has 4 bits.
/// </summary>
Bit12 = 12,
/// <summary>
/// 24 bits per sample, each color channel has 8 Bits.
/// </summary>
Bit24 = 24,
/// <summary>
/// Thirty bits per sample, each channel has 10 bits.
/// </summary>
Bit30 = 30,
/// <summary>
/// Forty two bits per sample, each channel has 14 bits.
/// </summary>
Bit42 = 42,
}
}

40
src/ImageSharp/Formats/Tiff/TiffBitsPerSampleExtensions.cs

@ -21,10 +21,18 @@ namespace SixLabors.ImageSharp.Formats.Tiff
return TiffConstants.BitsPerSample1Bit;
case TiffBitsPerSample.Bit4:
return TiffConstants.BitsPerSample4Bit;
case TiffBitsPerSample.Bit6:
return TiffConstants.BitsPerSampleRgb2Bit;
case TiffBitsPerSample.Bit8:
return TiffConstants.BitsPerSample8Bit;
case TiffBitsPerSample.Bit12:
return TiffConstants.BitsPerSampleRgb4Bit;
case TiffBitsPerSample.Bit24:
return TiffConstants.BitsPerSampleRgb8Bit;
case TiffBitsPerSample.Bit30:
return TiffConstants.BitsPerSampleRgb10Bit;
case TiffBitsPerSample.Bit42:
return TiffConstants.BitsPerSampleRgb14Bit;
default:
return Array.Empty<ushort>();
@ -41,13 +49,41 @@ namespace SixLabors.ImageSharp.Formats.Tiff
switch (bitsPerSample.Length)
{
case 3:
if (bitsPerSample[0] == TiffConstants.BitsPerSampleRgb8Bit[0] &&
if (bitsPerSample[2] == TiffConstants.BitsPerSampleRgb14Bit[2] &&
bitsPerSample[1] == TiffConstants.BitsPerSampleRgb14Bit[1] &&
bitsPerSample[0] == TiffConstants.BitsPerSampleRgb14Bit[0])
{
return TiffBitsPerSample.Bit42;
}
if (bitsPerSample[2] == TiffConstants.BitsPerSampleRgb10Bit[2] &&
bitsPerSample[1] == TiffConstants.BitsPerSampleRgb10Bit[1] &&
bitsPerSample[0] == TiffConstants.BitsPerSampleRgb10Bit[0])
{
return TiffBitsPerSample.Bit30;
}
if (bitsPerSample[2] == TiffConstants.BitsPerSampleRgb8Bit[2] &&
bitsPerSample[1] == TiffConstants.BitsPerSampleRgb8Bit[1] &&
bitsPerSample[2] == TiffConstants.BitsPerSampleRgb8Bit[2])
bitsPerSample[0] == TiffConstants.BitsPerSampleRgb8Bit[0])
{
return TiffBitsPerSample.Bit24;
}
if (bitsPerSample[2] == TiffConstants.BitsPerSampleRgb4Bit[2] &&
bitsPerSample[1] == TiffConstants.BitsPerSampleRgb4Bit[1] &&
bitsPerSample[0] == TiffConstants.BitsPerSampleRgb4Bit[0])
{
return TiffBitsPerSample.Bit12;
}
if (bitsPerSample[2] == TiffConstants.BitsPerSampleRgb2Bit[2] &&
bitsPerSample[1] == TiffConstants.BitsPerSampleRgb2Bit[1] &&
bitsPerSample[0] == TiffConstants.BitsPerSampleRgb2Bit[0])
{
return TiffBitsPerSample.Bit6;
}
break;
case 1:

4
src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

@ -202,7 +202,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
if (this.PlanarConfiguration == TiffPlanarConfiguration.Chunky)
{
DebugGuard.IsTrue(plane == -1, "Excepted Chunky planar.");
DebugGuard.IsTrue(plane == -1, "Expected Chunky planar.");
bitsPerPixel = this.BitsPerPixel;
}
else
@ -294,7 +294,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
int top = rowsPerStrip * stripIndex;
if (top + stripHeight > frame.Height)
{
// Make sure we ignore any strips that are not needed for the image (if too many are present)
// Make sure we ignore any strips that are not needed for the image (if too many are present).
break;
}

31
src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Linq;
using SixLabors.ImageSharp.Formats.Tiff.Compression;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
@ -179,7 +180,29 @@ namespace SixLabors.ImageSharp.Formats.Tiff
if (options.PlanarConfiguration == TiffPlanarConfiguration.Chunky)
{
options.ColorType = options.BitsPerSample == TiffBitsPerSample.Bit24 ? TiffColorType.Rgb888 : TiffColorType.Rgb;
switch (options.BitsPerSample)
{
case TiffBitsPerSample.Bit42:
options.ColorType = TiffColorType.Rgb141414;
break;
case TiffBitsPerSample.Bit30:
options.ColorType = TiffColorType.Rgb101010;
break;
case TiffBitsPerSample.Bit24:
options.ColorType = TiffColorType.Rgb888;
break;
case TiffBitsPerSample.Bit12:
options.ColorType = TiffColorType.Rgb444;
break;
case TiffBitsPerSample.Bit6:
options.ColorType = TiffColorType.Rgb222;
break;
default:
TiffThrowHelper.ThrowNotSupported("Bits per sample is not supported.");
break;
}
}
else
{
@ -273,9 +296,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff
{
TiffBitsPerPixel.Bit1 => TiffBitsPerSample.Bit1,
TiffBitsPerPixel.Bit4 => TiffBitsPerSample.Bit4,
TiffBitsPerPixel.Bit6 => TiffBitsPerSample.Bit6,
TiffBitsPerPixel.Bit8 => TiffBitsPerSample.Bit8,
TiffBitsPerPixel.Bit12 => TiffBitsPerSample.Bit12,
TiffBitsPerPixel.Bit24 => TiffBitsPerSample.Bit24,
_ => TiffBitsPerSample.Bit24,
TiffBitsPerPixel.Bit30 => TiffBitsPerSample.Bit30,
TiffBitsPerPixel.Bit42 => TiffBitsPerSample.Bit42,
_ => throw new NotSupportedException("The bits per pixel are not supported"),
};
}
}

9
src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs

@ -306,7 +306,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
case TiffBitsPerPixel.Bit1:
if (compression == TiffCompression.Ccitt1D || compression == TiffCompression.CcittGroup3Fax || compression == TiffCompression.CcittGroup4Fax)
{
// The “normal” PhotometricInterpretation for bilevel CCITT compressed data is WhiteIsZero.
// The “normal” PhotometricInterpretation for bilevel CCITT compressed data is WhiteIsZero.
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.WhiteIsZero, compression, TiffPredictor.None);
break;
}
@ -319,6 +319,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff
case TiffBitsPerPixel.Bit8:
this.SetEncoderOptions(bitsPerPixel, photometricInterpretation ?? TiffPhotometricInterpretation.BlackIsZero, compression, predictor);
break;
case TiffBitsPerPixel.Bit6:
case TiffBitsPerPixel.Bit12:
case TiffBitsPerPixel.Bit30:
case TiffBitsPerPixel.Bit42:
// Encoding 42, 30, 12 and 6 bits per pixel is not yet supported. Default to 24 bits.
this.SetEncoderOptions(TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, compression, TiffPredictor.None);
break;
default:
this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.Rgb, compression, predictor);
break;

11
src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs

@ -66,8 +66,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff
Value = SoftwareValue
};
this.collector.Add(width);
this.collector.Add(height);
this.collector.AddOrReplace(width);
this.collector.AddOrReplace(height);
this.ProcessResolution(image.Metadata, rootFrameExifProfile);
this.ProcessProfiles(image.Metadata, rootFrameExifProfile, rootFrameXmpBytes);
@ -227,7 +227,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff
exifProfile.RemoveValue(ExifTag.IccProfile);
}
TiffMetadata tiffMetadata = imageMetadata.GetTiffMetadata();
if (xmpProfile != null)
{
var xmp = new ExifByteArray(ExifTagValue.XMP, ExifDataType.Byte)
@ -252,6 +251,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
public void Process(TiffEncoderCore encoder)
{
var planarConfig = new ExifShort(ExifTagValue.PlanarConfiguration)
{
Value = (ushort)TiffPlanarConfiguration.Chunky
};
var samplesPerPixel = new ExifLong(ExifTagValue.SamplesPerPixel)
{
Value = GetSamplesPerPixel(encoder)
@ -274,6 +278,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
Value = (ushort)encoder.PhotometricInterpretation
};
this.collector.AddOrReplace(planarConfig);
this.collector.AddOrReplace(samplesPerPixel);
this.collector.AddOrReplace(bitPerSample);
this.collector.AddOrReplace(compression);

33
src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs

@ -55,23 +55,38 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers
/// <inheritdoc />
protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor)
{
Span<byte> pixels = GetStripPixels(((IPixelSource)this.quantizedImage).PixelBuffer, y, height);
Span<byte> indexedPixels = GetStripPixels(((IPixelSource)this.quantizedImage).PixelBuffer, y, height);
if (this.BitsPerPixel == 4)
{
using IMemoryOwner<byte> rows4bitBuffer = this.MemoryAllocator.Allocate<byte>(pixels.Length / 2);
int width = this.Image.Width;
int halfWidth = width >> 1;
int excess = (width & 1) * height; // (width % 2) * height
int rows4BitBufferLength = (halfWidth * height) + excess;
using IMemoryOwner<byte> rows4bitBuffer = this.MemoryAllocator.Allocate<byte>(rows4BitBufferLength);
Span<byte> rows4bit = rows4bitBuffer.GetSpan();
int idx = 0;
for (int i = 0; i < rows4bit.Length; i++)
int idxPixels = 0;
int idx4bitRows = 0;
for (int row = 0; row < height; row++)
{
rows4bit[i] = (byte)((pixels[idx] << 4) | (pixels[idx + 1] & 0xF));
idx += 2;
for (int x = 0; x < halfWidth; x++)
{
rows4bit[idx4bitRows] = (byte)((indexedPixels[idxPixels] << 4) | (indexedPixels[idxPixels + 1] & 0xF));
idxPixels += 2;
idx4bitRows++;
}
// Make sure rows are byte-aligned.
if (width % 2 != 0)
{
rows4bit[idx4bitRows++] = (byte)(indexedPixels[idxPixels++] << 4);
}
}
compressor.CompressStrip(rows4bit, height);
compressor.CompressStrip(rows4bit.Slice(0, idx4bitRows), height);
}
else
{
compressor.CompressStrip(pixels, height);
compressor.CompressStrip(indexedPixels, height);
}
}
@ -91,7 +106,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers
PixelOperations<TPixel>.Instance.ToRgb48(this.Configuration, quantizedColors, quantizedColorRgb48);
// It can happen that the quantized colors are less than the expected maximum per channel.
var diffToMaxColors = this.maxColors - quantizedColors.Length;
int diffToMaxColors = this.maxColors - quantizedColors.Length;
// In a TIFF ColorMap, all the Red values come first, followed by the Green values,
// then the Blue values. Convert the quantized palette to this format.

23
src/ImageSharp/Image.cs

@ -3,6 +3,7 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced;
@ -19,6 +20,8 @@ namespace SixLabors.ImageSharp
/// </summary>
public abstract partial class Image : IImage, IConfigurationProvider
{
private bool isDisposed;
private Size size;
private readonly Configuration configuration;
@ -80,8 +83,15 @@ namespace SixLabors.ImageSharp
/// <inheritdoc />
public void Dispose()
{
if (this.isDisposed)
{
return;
}
this.Dispose(true);
GC.SuppressFinalize(this);
this.isDisposed = true;
}
/// <summary>
@ -89,7 +99,7 @@ namespace SixLabors.ImageSharp
/// </summary>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream or encoder is null.</exception>
/// <exception cref="ArgumentNullException">Thrown if the stream or encoder is null.</exception>
public void Save(Stream stream, IImageEncoder encoder)
{
Guard.NotNull(stream, nameof(stream));
@ -148,7 +158,13 @@ namespace SixLabors.ImageSharp
/// <summary>
/// Throws <see cref="ObjectDisposedException"/> if the image is disposed.
/// </summary>
internal abstract void EnsureNotDisposed();
internal void EnsureNotDisposed()
{
if (this.isDisposed)
{
ThrowObjectDisposedException(this.GetType());
}
}
/// <summary>
/// Accepts a <see cref="IImageVisitor"/>.
@ -167,6 +183,9 @@ namespace SixLabors.ImageSharp
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
internal abstract Task AcceptAsync(IImageVisitorAsync visitor, CancellationToken cancellationToken);
[MethodImpl(InliningOptions.ColdPath)]
private static void ThrowObjectDisposedException(Type type) => throw new ObjectDisposedException(type.Name);
private class EncodeVisitor : IImageVisitor, IImageVisitorAsync
{
private readonly IImageEncoder encoder;

108
src/ImageSharp/ImageFrameCollection.cs

@ -4,6 +4,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp
{
@ -11,8 +12,10 @@ namespace SixLabors.ImageSharp
/// Encapsulates a pixel-agnostic collection of <see cref="ImageFrame"/> instances
/// that make up an <see cref="Image"/>.
/// </summary>
public abstract class ImageFrameCollection : IEnumerable<ImageFrame>
public abstract class ImageFrameCollection : IDisposable, IEnumerable<ImageFrame>
{
private bool isDisposed;
/// <summary>
/// Gets the number of frames.
/// </summary>
@ -21,7 +24,15 @@ namespace SixLabors.ImageSharp
/// <summary>
/// Gets the root frame.
/// </summary>
public ImageFrame RootFrame => this.NonGenericRootFrame;
public ImageFrame RootFrame
{
get
{
this.EnsureNotDisposed();
return this.NonGenericRootFrame;
}
}
/// <summary>
/// Gets the root frame. (Implements <see cref="RootFrame"/>.)
@ -36,7 +47,15 @@ namespace SixLabors.ImageSharp
/// </value>
/// <param name="index">The index.</param>
/// <returns>The <see cref="ImageFrame"/> at the specified index.</returns>
public ImageFrame this[int index] => this.NonGenericGetFrame(index);
public ImageFrame this[int index]
{
get
{
this.EnsureNotDisposed();
return this.NonGenericGetFrame(index);
}
}
/// <summary>
/// Determines the index of a specific <paramref name="frame"/> in the <seealso cref="ImageFrameCollection"/>.
@ -52,14 +71,24 @@ namespace SixLabors.ImageSharp
/// <param name="source">The <seealso cref="ImageFrame"/> to clone and insert into the <seealso cref="ImageFrameCollection"/>.</param>
/// <exception cref="ArgumentException">Frame must have the same dimensions as the image.</exception>
/// <returns>The cloned <see cref="ImageFrame"/>.</returns>
public ImageFrame InsertFrame(int index, ImageFrame source) => this.NonGenericInsertFrame(index, source);
public ImageFrame InsertFrame(int index, ImageFrame source)
{
this.EnsureNotDisposed();
return this.NonGenericInsertFrame(index, source);
}
/// <summary>
/// Clones the <paramref name="source"/> frame and appends the clone to the end of the collection.
/// </summary>
/// <param name="source">The raw pixel data to generate the <seealso cref="ImageFrame{TPixel}"/> from.</param>
/// <returns>The cloned <see cref="ImageFrame{TPixel}"/>.</returns>
public ImageFrame AddFrame(ImageFrame source) => this.NonGenericAddFrame(source);
public ImageFrame AddFrame(ImageFrame source)
{
this.EnsureNotDisposed();
return this.NonGenericAddFrame(source);
}
/// <summary>
/// Removes the frame at the specified index and frees all freeable resources associated with it.
@ -91,7 +120,12 @@ namespace SixLabors.ImageSharp
/// <param name="index">The zero-based index of the frame to export.</param>
/// <exception cref="InvalidOperationException">Cannot remove last frame.</exception>
/// <returns>The new <see cref="Image{TPixel}"/> with the specified frame.</returns>
public Image ExportFrame(int index) => this.NonGenericExportFrame(index);
public Image ExportFrame(int index)
{
this.EnsureNotDisposed();
return this.NonGenericExportFrame(index);
}
/// <summary>
/// Creates an <see cref="Image{T}"/> with only the frame at the specified index
@ -99,7 +133,12 @@ namespace SixLabors.ImageSharp
/// </summary>
/// <param name="index">The zero-based index of the frame to clone.</param>
/// <returns>The new <see cref="Image{TPixel}"/> with the specified frame.</returns>
public Image CloneFrame(int index) => this.NonGenericCloneFrame(index);
public Image CloneFrame(int index)
{
this.EnsureNotDisposed();
return this.NonGenericCloneFrame(index);
}
/// <summary>
/// Creates a new <seealso cref="ImageFrame{TPixel}" /> and appends it to the end of the collection.
@ -107,7 +146,12 @@ namespace SixLabors.ImageSharp
/// <returns>
/// The new <see cref="ImageFrame{TPixel}" />.
/// </returns>
public ImageFrame CreateFrame() => this.NonGenericCreateFrame();
public ImageFrame CreateFrame()
{
this.EnsureNotDisposed();
return this.NonGenericCreateFrame();
}
/// <summary>
/// Creates a new <seealso cref="ImageFrame{TPixel}" /> and appends it to the end of the collection.
@ -116,14 +160,55 @@ namespace SixLabors.ImageSharp
/// <returns>
/// The new <see cref="ImageFrame{TPixel}" />.
/// </returns>
public ImageFrame CreateFrame(Color backgroundColor) => this.NonGenericCreateFrame(backgroundColor);
public ImageFrame CreateFrame(Color backgroundColor)
{
this.EnsureNotDisposed();
return this.NonGenericCreateFrame(backgroundColor);
}
/// <inheritdoc />
public IEnumerator<ImageFrame> GetEnumerator() => this.NonGenericGetEnumerator();
public void Dispose()
{
if (this.isDisposed)
{
return;
}
this.Dispose(true);
GC.SuppressFinalize(this);
this.isDisposed = true;
}
/// <inheritdoc />
public IEnumerator<ImageFrame> GetEnumerator()
{
this.EnsureNotDisposed();
return this.NonGenericGetEnumerator();
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
/// <summary>
/// Throws <see cref="ObjectDisposedException"/> if the image frame is disposed.
/// </summary>
protected void EnsureNotDisposed()
{
if (this.isDisposed)
{
ThrowObjectDisposedException(this.GetType());
}
}
/// <summary>
/// Disposes the object and frees resources for the Garbage Collector.
/// </summary>
/// <param name="disposing">Whether to dispose of managed and unmanaged objects.</param>
protected abstract void Dispose(bool disposing);
/// <summary>
/// Implements <see cref="GetEnumerator"/>.
/// </summary>
@ -178,5 +263,8 @@ namespace SixLabors.ImageSharp
/// <param name="backgroundColor">The background color.</param>
/// <returns>The new frame.</returns>
protected abstract ImageFrame NonGenericCreateFrame(Color backgroundColor);
[MethodImpl(InliningOptions.ColdPath)]
private static void ThrowObjectDisposedException(Type type) => throw new ObjectDisposedException(type.Name);
}
}

88
src/ImageSharp/ImageFrameCollection{TPixel}.cs

@ -67,7 +67,26 @@ namespace SixLabors.ImageSharp
/// <summary>
/// Gets the root frame.
/// </summary>
public new ImageFrame<TPixel> RootFrame => this.frames.Count > 0 ? this.frames[0] : null;
public new ImageFrame<TPixel> RootFrame
{
get
{
this.EnsureNotDisposed();
// frame collection would always contain at least 1 frame
// the only exception is when collection is disposed what is checked via EnsureNotDisposed() call
return this.frames[0];
}
}
/// <summary>
/// Gets root frame accessor in unsafe manner without any checks.
/// </summary>
/// <remarks>
/// This property is most likely to be called from <see cref="Image{TPixel}"/> for indexing pixels.
/// <see cref="Image{TPixel}"/> already checks if it was disposed before querying for root frame.
/// </remarks>
internal ImageFrame<TPixel> RootFrameUnsafe => this.frames[0];
/// <inheritdoc />
protected override ImageFrame NonGenericRootFrame => this.RootFrame;
@ -80,12 +99,22 @@ namespace SixLabors.ImageSharp
/// </value>
/// <param name="index">The index.</param>
/// <returns>The <see cref="ImageFrame{TPixel}"/> at the specified index.</returns>
public new ImageFrame<TPixel> this[int index] => this.frames[index];
public new ImageFrame<TPixel> this[int index]
{
get
{
this.EnsureNotDisposed();
return this.frames[index];
}
}
/// <inheritdoc />
public override int IndexOf(ImageFrame frame)
{
return frame is ImageFrame<TPixel> specific ? this.IndexOf(specific) : -1;
this.EnsureNotDisposed();
return frame is ImageFrame<TPixel> specific ? this.frames.IndexOf(specific) : -1;
}
/// <summary>
@ -93,7 +122,12 @@ namespace SixLabors.ImageSharp
/// </summary>
/// <param name="frame">The <seealso cref="ImageFrame{TPixel}"/> to locate in the <seealso cref="ImageFrameCollection{TPixel}"/>.</param>
/// <returns>The index of item if found in the list; otherwise, -1.</returns>
public int IndexOf(ImageFrame<TPixel> frame) => this.frames.IndexOf(frame);
public int IndexOf(ImageFrame<TPixel> frame)
{
this.EnsureNotDisposed();
return this.frames.IndexOf(frame);
}
/// <summary>
/// Clones and inserts the <paramref name="source"/> into the <seealso cref="ImageFrameCollection{TPixel}"/> at the specified <paramref name="index"/>.
@ -104,6 +138,8 @@ namespace SixLabors.ImageSharp
/// <returns>The cloned <see cref="ImageFrame{TPixel}"/>.</returns>
public ImageFrame<TPixel> InsertFrame(int index, ImageFrame<TPixel> source)
{
this.EnsureNotDisposed();
this.ValidateFrame(source);
ImageFrame<TPixel> clonedFrame = source.Clone(this.parent.GetConfiguration());
this.frames.Insert(index, clonedFrame);
@ -117,6 +153,8 @@ namespace SixLabors.ImageSharp
/// <returns>The cloned <see cref="ImageFrame{TPixel}"/>.</returns>
public ImageFrame<TPixel> AddFrame(ImageFrame<TPixel> source)
{
this.EnsureNotDisposed();
this.ValidateFrame(source);
ImageFrame<TPixel> clonedFrame = source.Clone(this.parent.GetConfiguration());
this.frames.Add(clonedFrame);
@ -131,6 +169,8 @@ namespace SixLabors.ImageSharp
/// <returns>The new <see cref="ImageFrame{TPixel}"/>.</returns>
public ImageFrame<TPixel> AddFrame(ReadOnlySpan<TPixel> source)
{
this.EnsureNotDisposed();
var frame = ImageFrame.LoadPixelData(
this.parent.GetConfiguration(),
source,
@ -149,6 +189,7 @@ namespace SixLabors.ImageSharp
public ImageFrame<TPixel> AddFrame(TPixel[] source)
{
Guard.NotNull(source, nameof(source));
return this.AddFrame(source.AsSpan());
}
@ -159,6 +200,8 @@ namespace SixLabors.ImageSharp
/// <exception cref="InvalidOperationException">Cannot remove last frame.</exception>
public override void RemoveFrame(int index)
{
this.EnsureNotDisposed();
if (index == 0 && this.Count == 1)
{
throw new InvalidOperationException("Cannot remove last frame.");
@ -170,8 +213,12 @@ namespace SixLabors.ImageSharp
}
/// <inheritdoc />
public override bool Contains(ImageFrame frame) =>
frame is ImageFrame<TPixel> specific && this.Contains(specific);
public override bool Contains(ImageFrame frame)
{
this.EnsureNotDisposed();
return frame is ImageFrame<TPixel> specific && this.frames.Contains(specific);
}
/// <summary>
/// Determines whether the <seealso cref="ImageFrameCollection{TPixel}"/> contains the <paramref name="frame"/>.
@ -180,7 +227,12 @@ namespace SixLabors.ImageSharp
/// <returns>
/// <c>true</c> if the <seealso cref="ImageFrameCollection{TPixel}"/> contains the specified frame; otherwise, <c>false</c>.
/// </returns>
public bool Contains(ImageFrame<TPixel> frame) => this.frames.Contains(frame);
public bool Contains(ImageFrame<TPixel> frame)
{
this.EnsureNotDisposed();
return this.frames.Contains(frame);
}
/// <summary>
/// Moves an <seealso cref="ImageFrame{TPixel}"/> from <paramref name="sourceIndex"/> to <paramref name="destinationIndex"/>.
@ -189,6 +241,8 @@ namespace SixLabors.ImageSharp
/// <param name="destinationIndex">The index to move the frame to.</param>
public override void MoveFrame(int sourceIndex, int destinationIndex)
{
this.EnsureNotDisposed();
if (sourceIndex == destinationIndex)
{
return;
@ -208,6 +262,8 @@ namespace SixLabors.ImageSharp
/// <returns>The new <see cref="Image{TPixel}"/> with the specified frame.</returns>
public new Image<TPixel> ExportFrame(int index)
{
this.EnsureNotDisposed();
ImageFrame<TPixel> frame = this[index];
if (this.Count == 1 && this.frames.Contains(frame))
@ -228,6 +284,8 @@ namespace SixLabors.ImageSharp
/// <returns>The new <see cref="Image{TPixel}"/> with the specified frame.</returns>
public new Image<TPixel> CloneFrame(int index)
{
this.EnsureNotDisposed();
ImageFrame<TPixel> frame = this[index];
ImageFrame<TPixel> clonedFrame = frame.Clone();
return new Image<TPixel>(this.parent.GetConfiguration(), this.parent.Metadata.DeepClone(), new[] { clonedFrame });
@ -241,6 +299,8 @@ namespace SixLabors.ImageSharp
/// </returns>
public new ImageFrame<TPixel> CreateFrame()
{
this.EnsureNotDisposed();
var frame = new ImageFrame<TPixel>(
this.parent.GetConfiguration(),
this.RootFrame.Width,
@ -335,14 +395,18 @@ namespace SixLabors.ImageSharp
}
}
internal void Dispose()
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
foreach (ImageFrame<TPixel> f in this.frames)
if (disposing)
{
f.Dispose();
}
foreach (ImageFrame<TPixel> f in this.frames)
{
f.Dispose();
}
this.frames.Clear();
this.frames.Clear();
}
}
private ImageFrame<TPixel> CopyNonCompatibleFrame(ImageFrame source)

66
src/ImageSharp/Image{TPixel}.cs

@ -23,7 +23,7 @@ namespace SixLabors.ImageSharp
public sealed class Image<TPixel> : Image
where TPixel : unmanaged, IPixel<TPixel>
{
private bool isDisposed;
private readonly ImageFrameCollection<TPixel> frames;
/// <summary>
/// Initializes a new instance of the <see cref="Image{TPixel}"/> class
@ -84,7 +84,7 @@ namespace SixLabors.ImageSharp
internal Image(Configuration configuration, int width, int height, ImageMetadata metadata)
: base(configuration, PixelTypeInfo.Create<TPixel>(), metadata, width, height)
{
this.Frames = new ImageFrameCollection<TPixel>(this, width, height, default(TPixel));
this.frames = new ImageFrameCollection<TPixel>(this, width, height, default(TPixel));
}
/// <summary>
@ -104,7 +104,7 @@ namespace SixLabors.ImageSharp
ImageMetadata metadata)
: base(configuration, PixelTypeInfo.Create<TPixel>(), metadata, width, height)
{
this.Frames = new ImageFrameCollection<TPixel>(this, width, height, memoryGroup);
this.frames = new ImageFrameCollection<TPixel>(this, width, height, memoryGroup);
}
/// <summary>
@ -124,7 +124,7 @@ namespace SixLabors.ImageSharp
ImageMetadata metadata)
: base(configuration, PixelTypeInfo.Create<TPixel>(), metadata, width, height)
{
this.Frames = new ImageFrameCollection<TPixel>(this, width, height, backgroundColor);
this.frames = new ImageFrameCollection<TPixel>(this, width, height, backgroundColor);
}
/// <summary>
@ -137,7 +137,7 @@ namespace SixLabors.ImageSharp
internal Image(Configuration configuration, ImageMetadata metadata, IEnumerable<ImageFrame<TPixel>> frames)
: base(configuration, PixelTypeInfo.Create<TPixel>(), metadata, ValidateFramesAndGetSize(frames))
{
this.Frames = new ImageFrameCollection<TPixel>(this, frames);
this.frames = new ImageFrameCollection<TPixel>(this, frames);
}
/// <inheritdoc />
@ -146,12 +146,19 @@ namespace SixLabors.ImageSharp
/// <summary>
/// Gets the collection of image frames.
/// </summary>
public new ImageFrameCollection<TPixel> Frames { get; }
public new ImageFrameCollection<TPixel> Frames
{
get
{
this.EnsureNotDisposed();
return this.frames;
}
}
/// <summary>
/// Gets the root frame.
/// </summary>
private IPixelSource<TPixel> PixelSource => this.Frames?.RootFrame ?? throw new ObjectDisposedException(nameof(Image<TPixel>));
private IPixelSource<TPixel> PixelSourceUnsafe => this.frames.RootFrameUnsafe;
/// <summary>
/// Gets or sets the pixel at the specified position.
@ -165,15 +172,19 @@ namespace SixLabors.ImageSharp
[MethodImpl(InliningOptions.ShortMethod)]
get
{
this.EnsureNotDisposed();
this.VerifyCoords(x, y);
return this.PixelSource.PixelBuffer.GetElementUnsafe(x, y);
return this.PixelSourceUnsafe.PixelBuffer.GetElementUnsafe(x, y);
}
[MethodImpl(InliningOptions.ShortMethod)]
set
{
this.EnsureNotDisposed();
this.VerifyCoords(x, y);
this.PixelSource.PixelBuffer.GetElementUnsafe(x, y) = value;
this.PixelSourceUnsafe.PixelBuffer.GetElementUnsafe(x, y) = value;
}
}
@ -189,7 +200,9 @@ namespace SixLabors.ImageSharp
Guard.MustBeGreaterThanOrEqualTo(rowIndex, 0, nameof(rowIndex));
Guard.MustBeLessThan(rowIndex, this.Height, nameof(rowIndex));
return this.PixelSource.PixelBuffer.GetRowSpan(rowIndex);
this.EnsureNotDisposed();
return this.PixelSourceUnsafe.PixelBuffer.GetRowSpan(rowIndex);
}
/// <summary>
@ -226,10 +239,10 @@ namespace SixLabors.ImageSharp
{
this.EnsureNotDisposed();
var clonedFrames = new ImageFrame<TPixel>[this.Frames.Count];
var clonedFrames = new ImageFrame<TPixel>[this.frames.Count];
for (int i = 0; i < clonedFrames.Length; i++)
{
clonedFrames[i] = this.Frames[i].Clone(configuration);
clonedFrames[i] = this.frames[i].Clone(configuration);
}
return new Image<TPixel>(configuration, this.Metadata.DeepClone(), clonedFrames);
@ -245,10 +258,10 @@ namespace SixLabors.ImageSharp
{
this.EnsureNotDisposed();
var clonedFrames = new ImageFrame<TPixel2>[this.Frames.Count];
var clonedFrames = new ImageFrame<TPixel2>[this.frames.Count];
for (int i = 0; i < clonedFrames.Length; i++)
{
clonedFrames[i] = this.Frames[i].CloneAs<TPixel2>(configuration);
clonedFrames[i] = this.frames[i].CloneAs<TPixel2>(configuration);
}
return new Image<TPixel2>(configuration, this.Metadata.DeepClone(), clonedFrames);
@ -257,25 +270,9 @@ namespace SixLabors.ImageSharp
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (this.isDisposed)
{
return;
}
if (disposing)
{
this.Frames.Dispose();
}
this.isDisposed = true;
}
/// <inheritdoc/>
internal override void EnsureNotDisposed()
{
if (this.isDisposed)
{
throw new ObjectDisposedException("Trying to execute an operation on a disposed image.");
this.frames.Dispose();
}
}
@ -306,9 +303,12 @@ namespace SixLabors.ImageSharp
{
Guard.NotNull(pixelSource, nameof(pixelSource));
for (int i = 0; i < this.Frames.Count; i++)
this.EnsureNotDisposed();
ImageFrameCollection<TPixel> sourceFrames = pixelSource.Frames;
for (int i = 0; i < this.frames.Count; i++)
{
this.Frames[i].SwapOrCopyPixelsBufferFrom(pixelSource.Frames[i]);
this.frames[i].SwapOrCopyPixelsBufferFrom(sourceFrames[i]);
}
this.UpdateSize(pixelSource.Size());

9
src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor.cs

@ -22,10 +22,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
bool clipHistogram,
int clipLimit,
int numberOfTiles)
: base(luminanceLevels, clipHistogram, clipLimit)
{
this.NumberOfTiles = numberOfTiles;
}
: base(luminanceLevels, clipHistogram, clipLimit) => this.NumberOfTiles = numberOfTiles;
/// <summary>
/// Gets the number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization.
@ -34,8 +31,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// <inheritdoc />
public override IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)
{
return new AdaptiveHistogramEqualizationProcessor<TPixel>(
=> new AdaptiveHistogramEqualizationProcessor<TPixel>(
configuration,
this.LuminanceLevels,
this.ClipHistogram,
@ -43,6 +39,5 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
this.NumberOfTiles,
source,
sourceRectangle);
}
}
}

9
src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs

@ -459,10 +459,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
private readonly Configuration configuration;
private readonly MemoryAllocator memoryAllocator;
// Used for storing the minimum value for each CDF entry.
/// <summary>
/// Used for storing the minimum value for each CDF entry.
/// </summary>
private readonly Buffer2D<int> cdfMinBuffer2D;
// Used for storing the LUT for each CDF entry.
/// <summary>
/// Used for storing the LUT for each CDF entry.
/// </summary>
private readonly Buffer2D<int> cdfLutBuffer2D;
private readonly int pixelsInTile;
private readonly int sourceWidth;
@ -596,6 +600,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
int y = this.tileYStartPositions[index].y;
int endY = Math.Min(y + this.tileHeight, this.sourceHeight);
Span<int> cdfMinSpan = this.cdfMinBuffer2D.GetRowSpan(cdfY);
cdfMinSpan.Clear();
using IMemoryOwner<int> histogramBuffer = this.allocator.Allocate<int>(this.luminanceLevels);
Span<int> histogram = histogramBuffer.GetSpan();

44
src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs

@ -49,44 +49,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// </summary>
/// <param name="options">The <see cref="HistogramEqualizationOptions"/>.</param>
/// <returns>The <see cref="HistogramEqualizationProcessor"/>.</returns>
public static HistogramEqualizationProcessor FromOptions(HistogramEqualizationOptions options)
public static HistogramEqualizationProcessor FromOptions(HistogramEqualizationOptions options) => options.Method switch
{
HistogramEqualizationProcessor processor;
HistogramEqualizationMethod.Global
=> new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit),
switch (options.Method)
{
case HistogramEqualizationMethod.Global:
processor = new GlobalHistogramEqualizationProcessor(
options.LuminanceLevels,
options.ClipHistogram,
options.ClipLimit);
break;
HistogramEqualizationMethod.AdaptiveTileInterpolation
=> new AdaptiveHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit, options.NumberOfTiles),
case HistogramEqualizationMethod.AdaptiveTileInterpolation:
processor = new AdaptiveHistogramEqualizationProcessor(
options.LuminanceLevels,
options.ClipHistogram,
options.ClipLimit,
options.NumberOfTiles);
break;
HistogramEqualizationMethod.AdaptiveSlidingWindow
=> new AdaptiveHistogramEqualizationSlidingWindowProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit, options.NumberOfTiles),
case HistogramEqualizationMethod.AdaptiveSlidingWindow:
processor = new AdaptiveHistogramEqualizationSlidingWindowProcessor(
options.LuminanceLevels,
options.ClipHistogram,
options.ClipLimit,
options.NumberOfTiles);
break;
default:
processor = new GlobalHistogramEqualizationProcessor(
options.LuminanceLevels,
options.ClipHistogram,
options.ClipLimit);
break;
}
return processor;
}
_ => new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit),
};
}
}

1
src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs

@ -142,6 +142,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
[MethodImpl(InliningOptions.ShortMethod)]
public static int GetLuminance(TPixel sourcePixel, int luminanceLevels)
{
// TODO: We need a bulk per span equivalent.
var vector = sourcePixel.ToVector4();
return ColorNumerics.GetBT709Luminance(ref vector, luminanceLevels);
}

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

@ -12,6 +12,7 @@ using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using Xunit;
@ -127,60 +128,53 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
}
[Theory]
[InlineData(TestImages.Jpeg.Baseline.Jpeg420Small, 0)]
[InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 1)]
[InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 15)]
[InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 30)]
[InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 1)]
[InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 15)]
[InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 30)]
public async Task Decode_IsCancellable(string fileName, int cancellationDelayMs)
[InlineData(0)]
[InlineData(0.5)]
[InlineData(0.9)]
public async Task Decode_IsCancellable(int percentageOfStreamReadToCancel)
{
// Decoding these huge files took 300ms on i7-8650U in 2020. 30ms should be safe for cancellation delay.
string hugeFile = Path.Combine(
TestEnvironment.InputImagesDirectoryFullPath,
fileName);
const int numberOfRuns = 5;
for (int i = 0; i < numberOfRuns; i++)
var cts = new CancellationTokenSource();
var file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small);
using var pausedStream = new PausedStream(file);
pausedStream.OnWaiting(s =>
{
var cts = new CancellationTokenSource();
if (cancellationDelayMs == 0)
if (s.Position >= s.Length * percentageOfStreamReadToCancel)
{
cts.Cancel();
pausedStream.Release();
}
else
{
cts.CancelAfter(cancellationDelayMs);
}
try
{
using Image image = await Image.LoadAsync(hugeFile, cts.Token);
}
catch (TaskCanceledException)
{
// Successfully observed a cancellation
return;
// allows this/next wait to unblock
pausedStream.Next();
}
}
});
throw new Exception($"No cancellation happened out of {numberOfRuns} runs!");
var config = Configuration.CreateDefaultInstance();
config.FileSystem = new SingleStreamFileSystem(pausedStream);
await Assert.ThrowsAsync<TaskCanceledException>(async () =>
{
using Image image = await Image.LoadAsync(config, "someFakeFile", cts.Token);
});
}
[Theory(Skip = "Identify is too fast, doesn't work reliably.")]
[InlineData(TestImages.Jpeg.Baseline.Exif)]
[InlineData(TestImages.Jpeg.Progressive.Bad.ExifUndefType)]
public async Task Identify_IsCancellable(string fileName)
[Fact]
public async Task Identify_IsCancellable()
{
string file = Path.Combine(
TestEnvironment.InputImagesDirectoryFullPath,
fileName);
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromTicks(1));
await Assert.ThrowsAsync<TaskCanceledException>(() => Image.IdentifyAsync(file, cts.Token));
var file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small);
using var pausedStream = new PausedStream(file);
pausedStream.OnWaiting(s =>
{
cts.Cancel();
pausedStream.Release();
});
var config = Configuration.CreateDefaultInstance();
config.FileSystem = new SingleStreamFileSystem(pausedStream);
await Assert.ThrowsAsync<TaskCanceledException>(async () => await Image.IdentifyAsync(config, "someFakeFile", cts.Token));
}
// DEBUG ONLY!

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

@ -13,6 +13,7 @@ using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using Xunit;
@ -310,28 +311,33 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
}
[Theory]
[InlineData(JpegSubsample.Ratio420, 0)]
[InlineData(JpegSubsample.Ratio420, 3)]
[InlineData(JpegSubsample.Ratio420, 10)]
[InlineData(JpegSubsample.Ratio444, 0)]
[InlineData(JpegSubsample.Ratio444, 3)]
[InlineData(JpegSubsample.Ratio444, 10)]
public async Task Encode_IsCancellable(JpegSubsample subsample, int cancellationDelayMs)
[InlineData(JpegSubsample.Ratio420)]
[InlineData(JpegSubsample.Ratio444)]
public async Task Encode_IsCancellable(JpegSubsample subsample)
{
using var image = new Image<Rgba32>(5000, 5000);
using var stream = new MemoryStream();
var cts = new CancellationTokenSource();
if (cancellationDelayMs == 0)
{
cts.Cancel();
}
else
using var pausedStream = new PausedStream(new MemoryStream());
pausedStream.OnWaiting(s =>
{
cts.CancelAfter(cancellationDelayMs);
}
// after some writing
if (s.Position >= 500)
{
cts.Cancel();
pausedStream.Release();
}
else
{
// allows this/next wait to unblock
pausedStream.Next();
}
});
var encoder = new JpegEncoder() { Subsample = subsample };
await Assert.ThrowsAsync<TaskCanceledException>(() => image.SaveAsync(stream, encoder, cts.Token));
using var image = new Image<Rgba32>(5000, 5000);
await Assert.ThrowsAsync<TaskCanceledException>(async () =>
{
var encoder = new JpegEncoder() { Subsample = subsample };
await image.SaveAsync(pausedStream, encoder, cts.Token);
});
}
}
}

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

@ -4,6 +4,7 @@
// ReSharper disable InconsistentNaming
using System;
using System.IO;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
@ -34,6 +35,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
[InlineData(RgbUncompressed, 24, 256, 256, 300, 300, PixelResolutionUnit.PixelsPerInch)]
[InlineData(SmallRgbDeflate, 24, 32, 32, 96, 96, PixelResolutionUnit.PixelsPerInch)]
[InlineData(Calliphora_GrayscaleUncompressed, 8, 804, 1198, 96, 96, PixelResolutionUnit.PixelsPerInch)]
[InlineData(Flower4BitPalette, 4, 73, 43, 72, 72, PixelResolutionUnit.PixelsPerInch)]
public void Identify(string imagePath, int expectedPixelSize, int expectedWidth, int expectedHeight, double expectedHResolution, double expectedVResolution, PixelResolutionUnit expectedResolutionUnit)
{
var testFile = TestFile.Create(imagePath);
@ -88,6 +90,37 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
public void TiffDecoder_CanDecode_WithPalette<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(Rgb4BitPalette, PixelTypes.Rgba32)]
[WithFile(Flower4BitPalette, PixelTypes.Rgba32)]
[WithFile(Flower4BitPaletteGray, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_4Bit_WithPalette<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider, ReferenceDecoder, useExactComparer: false, 0.01f);
[Theory]
[WithFile(FlowerRgb222Contiguous, PixelTypes.Rgba32)]
[WithFile(FlowerRgb222Planar, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_6Bit<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(FlowerRgb444Contiguous, PixelTypes.Rgba32)]
[WithFile(FlowerRgb444Planar, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_12Bit<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(FlowerRgb101010Contiguous, PixelTypes.Rgba32)]
[WithFile(FlowerRgb101010Planar, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_30Bit<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(FlowerRgb141414Contiguous, PixelTypes.Rgba32)]
[WithFile(FlowerRgb141414Planar, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_42Bit<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFile(GrayscaleDeflateMultistrip, PixelTypes.Rgba32)]
[WithFile(RgbDeflateMultistrip, PixelTypes.Rgba32)]
@ -152,12 +185,15 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
image.CompareToOriginalMultiFrame(provider, ImageComparer.Exact, ReferenceDecoder);
}
private static void TestTiffDecoder<TPixel>(TestImageProvider<TPixel> provider)
private static void TestTiffDecoder<TPixel>(TestImageProvider<TPixel> provider, IImageDecoder referenceDecoder = null, bool useExactComparer = true, float compareTolerance = 0.001f)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(TiffDecoder);
image.DebugSave(provider);
image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder);
image.CompareToOriginal(
provider,
useExactComparer ? ImageComparer.Exact : ImageComparer.Tolerant(compareTolerance),
referenceDecoder ?? ReferenceDecoder);
}
}
}

36
tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs

@ -77,6 +77,29 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
Assert.Equal(TiffCompression.None, frameMetaData.Compression);
}
[Theory]
[InlineData(TiffBitsPerPixel.Bit42)]
[InlineData(TiffBitsPerPixel.Bit30)]
[InlineData(TiffBitsPerPixel.Bit12)]
[InlineData(TiffBitsPerPixel.Bit6)]
public void EncoderOptions_UnsupportedBitPerPixel_DefaultTo24Bits(TiffBitsPerPixel bitsPerPixel)
{
// arrange
var tiffEncoder = new TiffEncoder { BitsPerPixel = bitsPerPixel };
using Image input = new Image<Rgb24>(10, 10);
using var memStream = new MemoryStream();
// act
input.Save(memStream, tiffEncoder);
// assert
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata();
Assert.Equal(TiffBitsPerPixel.Bit24, frameMetaData.BitsPerPixel);
}
[Theory]
[InlineData(null, TiffCompression.Deflate, TiffBitsPerPixel.Bit24, TiffCompression.Deflate)]
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.Deflate, TiffBitsPerPixel.Bit24, TiffCompression.Deflate)]
@ -228,6 +251,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
Assert.Equal(expectedCompression, frameMetaData.Compression);
}
// This makes sure, that when decoding a planar tiff, the planar configuration is not carried over to the encoded image.
[Theory]
[WithFile(FlowerRgb444Planar, PixelTypes.Rgba32)]
public void TiffEncoder_EncodePlanar_AndReload_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, imageDecoder: new TiffDecoder());
[Theory]
[WithFile(Calliphora_RgbUncompressed, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeRgb_Works<TPixel>(TestImageProvider<TPixel> provider)
@ -296,10 +325,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
[Theory]
[WithFile(Rgb4BitPalette, PixelTypes.Rgba32)]
[WithFile(Flower4BitPalette, PixelTypes.Rgba32)]
[WithFile(Flower4BitPaletteGray, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeColorPalette_With4Bit_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> =>
//// Note: The magick reference decoder does not support 4 bit tiff's, so we use our TIFF decoder instead.
TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit4, TiffPhotometricInterpretation.PaletteColor, useExactComparer: false, compareTolerance: 0.001f, imageDecoder: new TiffDecoder());
TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit4, TiffPhotometricInterpretation.PaletteColor, useExactComparer: false, compareTolerance: 0.003f);
[Theory]
[WithFile(Calliphora_PaletteUncompressed, PixelTypes.Rgba32)]
@ -460,7 +490,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
TiffCompression compression = TiffCompression.None,
TiffPredictor predictor = TiffPredictor.None,
bool useExactComparer = true,
float compareTolerance = 0.01f,
float compareTolerance = 0.001f,
IImageDecoder imageDecoder = null)
where TPixel : unmanaged, IPixel<TPixel>
{

5
tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs

@ -288,10 +288,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
Assert.Equal("This is Изготовитель камеры", exifProfileInput.GetValue(ExifTag.Make).Value);
Assert.Equal("This is Авторские права", exifProfileInput.GetValue(ExifTag.Copyright).Value);
Assert.Equal(exifProfileInput.Values.Count, encodedImageExifProfile.Values.Count);
Assert.Equal(exifProfileInput.GetValue(ExifTag.ImageDescription).Value, encodedImageExifProfile.GetValue(ExifTag.ImageDescription).Value);
Assert.Equal(exifProfileInput.GetValue(ExifTag.Make).Value, encodedImageExifProfile.GetValue(ExifTag.Make).Value);
Assert.Equal(exifProfileInput.GetValue(ExifTag.Copyright).Value, encodedImageExifProfile.GetValue(ExifTag.Copyright).Value);
// Note that the encoded profile has PlanarConfiguration explicitly set, which is missing in the original image profile.
Assert.Equal((ushort)TiffPlanarConfiguration.Chunky, encodedImageExifProfile.GetValue(ExifTag.PlanarConfiguration)?.Value);
Assert.Equal(exifProfileInput.Values.Count + 1, encodedImageExifProfile.Values.Count);
}
}
}

42
tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs

@ -339,6 +339,48 @@ namespace SixLabors.ImageSharp.Tests
using var frame = new ImageFrame<Rgba32>(Configuration.Default, 10, 10);
Assert.False(this.Image.Frames.Contains(frame));
}
[Fact]
public void DisposeCall_NoThrowIfCalledMultiple()
{
var image = new Image<Rgba32>(Configuration.Default, 10, 10);
var frameCollection = image.Frames as ImageFrameCollection;
image.Dispose(); // this should invalidate underlying collection as well
frameCollection.Dispose();
}
[Fact]
public void PublicProperties_ThrowIfDisposed()
{
var image = new Image<Rgba32>(Configuration.Default, 10, 10);
var frameCollection = image.Frames as ImageFrameCollection;
image.Dispose(); // this should invalidate underlying collection as well
Assert.Throws<ObjectDisposedException>(() => { var prop = frameCollection.RootFrame; });
}
[Fact]
public void PublicMethods_ThrowIfDisposed()
{
var image = new Image<Rgba32>(Configuration.Default, 10, 10);
var frameCollection = image.Frames as ImageFrameCollection;
image.Dispose(); // this should invalidate underlying collection as well
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.AddFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.CloneFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.Contains(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.CreateFrame(); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.CreateFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.ExportFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.GetEnumerator(); });
Assert.Throws<ObjectDisposedException>(() => { var prop = frameCollection.IndexOf(default); });
Assert.Throws<ObjectDisposedException>(() => { var prop = frameCollection.InsertFrame(default, default); });
Assert.Throws<ObjectDisposedException>(() => { frameCollection.RemoveFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { frameCollection.MoveFrame(default, default); });
}
}
}
}

36
tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs

@ -263,6 +263,42 @@ namespace SixLabors.ImageSharp.Tests
Assert.False(this.Image.Frames.Contains(frame));
}
[Fact]
public void PublicProperties_ThrowIfDisposed()
{
var image = new Image<Rgba32>(Configuration.Default, 10, 10);
var frameCollection = image.Frames;
image.Dispose(); // this should invalidate underlying collection as well
Assert.Throws<ObjectDisposedException>(() => { var prop = frameCollection.RootFrame; });
}
[Fact]
public void PublicMethods_ThrowIfDisposed()
{
var image = new Image<Rgba32>(Configuration.Default, 10, 10);
var frameCollection = image.Frames;
var rgba32Array = new Rgba32[0];
image.Dispose(); // this should invalidate underlying collection as well
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.AddFrame((ImageFrame)null); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.AddFrame(rgba32Array); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.AddFrame((ImageFrame<Rgba32>)null); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.AddFrame(rgba32Array.AsSpan()); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.CloneFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.Contains(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.CreateFrame(); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.CreateFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.ExportFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = frameCollection.GetEnumerator(); });
Assert.Throws<ObjectDisposedException>(() => { var prop = frameCollection.IndexOf(default); });
Assert.Throws<ObjectDisposedException>(() => { var prop = frameCollection.InsertFrame(default, default); });
Assert.Throws<ObjectDisposedException>(() => { frameCollection.RemoveFrame(default); });
Assert.Throws<ObjectDisposedException>(() => { frameCollection.MoveFrame(default, default); });
}
/// <summary>
/// Integration test for end-to end API validation.
/// </summary>

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

@ -140,10 +140,15 @@ namespace SixLabors.ImageSharp.Tests
using var stream = new MemoryStream();
var asyncStream = new AsyncStreamWrapper(stream, () => false);
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromTicks(1));
await Assert.ThrowsAnyAsync<TaskCanceledException>(() =>
image.SaveAsync(asyncStream, encoder, cts.Token));
var pausedStream = new PausedStream(asyncStream);
pausedStream.OnWaiting(s =>
{
cts.Cancel();
pausedStream.Release();
});
await Assert.ThrowsAsync<TaskCanceledException>(async () => await image.SaveAsync(pausedStream, encoder, cts.Token));
}
}
}

69
tests/ImageSharp.Tests/Image/ImageTests.cs

@ -2,7 +2,9 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
@ -169,5 +171,72 @@ namespace SixLabors.ImageSharp.Tests
Assert.Equal("y", ex.ParamName);
}
}
public class Dispose
{
private readonly Configuration configuration = Configuration.CreateDefaultInstance();
public void MultipleDisposeCalls()
{
var image = new Image<Rgba32>(this.configuration, 10, 10);
image.Dispose();
image.Dispose();
}
[Fact]
public void NonPrivateProperties_ObjectDisposedException()
{
var image = new Image<Rgba32>(this.configuration, 10, 10);
var genericImage = (Image)image;
image.Dispose();
// Image<TPixel>
Assert.Throws<ObjectDisposedException>(() => { var prop = image.Frames; });
// Image
Assert.Throws<ObjectDisposedException>(() => { var prop = genericImage.Frames; });
}
[Fact]
public void Save_ObjectDisposedException()
{
using var stream = new MemoryStream();
var image = new Image<Rgba32>(this.configuration, 10, 10);
var encoder = new JpegEncoder();
image.Dispose();
// Image<TPixel>
Assert.Throws<ObjectDisposedException>(() => image.Save(stream, encoder));
}
[Fact]
public void AcceptVisitor_ObjectDisposedException()
{
// This test technically should exist but it's impossible to write proper test case without reflection:
// All visitor types are private and can't be created without context of some save/processing operation
// Save_ObjectDisposedException test checks this method with AcceptVisitor(EncodeVisitor) anyway
return;
}
[Fact]
public void NonPrivateMethods_ObjectDisposedException()
{
var image = new Image<Rgba32>(this.configuration, 10, 10);
var genericImage = (Image)image;
image.Dispose();
// Image<TPixel>
Assert.Throws<ObjectDisposedException>(() => { var res = image.Clone(this.configuration); });
Assert.Throws<ObjectDisposedException>(() => { var res = image.CloneAs<Rgba32>(this.configuration); });
Assert.Throws<ObjectDisposedException>(() => { var res = image.GetPixelRowSpan(default); });
Assert.Throws<ObjectDisposedException>(() => { var res = image.TryGetSinglePixelSpan(out var _); });
// Image
Assert.Throws<ObjectDisposedException>(() => { var res = genericImage.CloneAs<Rgba32>(this.configuration); });
}
}
}
}

39
tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs

@ -141,6 +141,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Normalization
/// See: https://github.com/SixLabors/ImageSharp/pull/984
/// </summary>
/// <typeparam name="TPixel">The pixel type of the image.</typeparam>
/// <param name="provider">The test image provider.</param>
[Theory]
[WithTestPatternImages(110, 110, PixelTypes.Rgb24)]
[WithTestPatternImages(170, 170, PixelTypes.Rgb24)]
@ -162,5 +163,43 @@ namespace SixLabors.ImageSharp.Tests.Processing.Normalization
image.CompareToReferenceOutput(ValidatorComparer, provider);
}
}
[Theory]
[WithTestPatternImages(5120, 9234, PixelTypes.L16)]
public unsafe void Issue1640<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
if (!TestEnvironment.Is64BitProcess)
{
return;
}
// https://github.com/SixLabors/ImageSharp/discussions/1640
// Test using isolated memory to ensure clean buffers for reference
provider.Configuration = Configuration.CreateDefaultInstance();
var options = new HistogramEqualizationOptions
{
Method = HistogramEqualizationMethod.AdaptiveTileInterpolation,
LuminanceLevels = 4096,
ClipHistogram = false,
ClipLimit = 350,
NumberOfTiles = 8
};
using Image<TPixel> image = provider.GetImage();
using Image<TPixel> referenceResult = image.Clone(ctx =>
{
ctx.HistogramEqualization(options);
ctx.Resize(image.Width / 4, image.Height / 4, KnownResamplers.Bicubic);
});
using Image<TPixel> processed = image.Clone(ctx =>
{
ctx.HistogramEqualization(options);
ctx.Resize(image.Width / 4, image.Height / 4, KnownResamplers.Bicubic);
});
ValidatorComparer.VerifySimilarity(referenceResult, processed);
}
}
}

10
tests/ImageSharp.Tests/TestImages.cs

@ -735,6 +735,16 @@ namespace SixLabors.ImageSharp.Tests
public const string RgbPalette = "Tiff/rgb_palette.tiff";
public const string Rgb4BitPalette = "Tiff/bike_colorpalette_4bit.tiff";
public const string RgbPaletteDeflate = "Tiff/rgb_palette_deflate.tiff";
public const string Flower4BitPalette = "Tiff/flower-palette-04.tiff";
public const string Flower4BitPaletteGray = "Tiff/flower-minisblack-04.tiff";
public const string FlowerRgb141414Contiguous = "Tiff/flower-rgb-contig-14.tiff";
public const string FlowerRgb141414Planar = "Tiff/flower-rgb-planar-14.tiff";
public const string FlowerRgb101010Contiguous = "Tiff/flower-rgb-contig-10.tiff";
public const string FlowerRgb101010Planar = "Tiff/flower-rgb-planar-10.tiff";
public const string FlowerRgb444Contiguous = "Tiff/flower-rgb-contig-04.tiff";
public const string FlowerRgb444Planar = "Tiff/flower-rgb-planar-04.tiff";
public const string FlowerRgb222Contiguous = "Tiff/flower-rgb-contig-02.tiff";
public const string FlowerRgb222Planar = "Tiff/flower-rgb-planar-02.tiff";
public const string SmallRgbDeflate = "Tiff/rgb_small_deflate.tiff";
public const string SmallRgbLzw = "Tiff/rgb_small_lzw.tiff";

145
tests/ImageSharp.Tests/TestUtilities/PausedStream.cs

@ -0,0 +1,145 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SixLabors.ImageSharp.Tests.TestUtilities
{
public class PausedStream : Stream
{
private readonly SemaphoreSlim semaphore = new SemaphoreSlim(0);
private readonly CancellationTokenSource cancelationTokenSource = new CancellationTokenSource();
private readonly Stream innerStream;
private Action<Stream> onWaitingCallback;
public void OnWaiting(Action<Stream> onWaitingCallback) => this.onWaitingCallback = onWaitingCallback;
public void OnWaiting(Action onWaitingCallback) => this.OnWaiting(_ => onWaitingCallback());
public void Release()
{
this.semaphore.Release();
this.cancelationTokenSource.Cancel();
}
public void Next() => this.semaphore.Release();
private void Wait()
{
if (this.cancelationTokenSource.IsCancellationRequested)
{
return;
}
this.onWaitingCallback?.Invoke(this.innerStream);
try
{
this.semaphore.Wait(this.cancelationTokenSource.Token);
}
catch (OperationCanceledException)
{
// ignore this as its just used to unlock any waits in progress
}
}
private async Task Await(Func<Task> action)
{
await Task.Yield();
this.Wait();
await action();
}
private async Task<T> Await<T>(Func<Task<T>> action)
{
await Task.Yield();
this.Wait();
return await action();
}
private T Await<T>(Func<T> action)
{
this.Wait();
return action();
}
private void Await(Action action)
{
this.Wait();
action();
}
public PausedStream(byte[] data)
: this(new MemoryStream(data))
{
}
public PausedStream(string filePath)
: this(File.OpenRead(filePath))
{
}
public PausedStream(Stream innerStream) => this.innerStream = innerStream;
public override bool CanTimeout => this.innerStream.CanTimeout;
public override void Close() => this.Await(() => this.innerStream.Close());
public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => this.Await(() => this.innerStream.CopyToAsync(destination, bufferSize, cancellationToken));
public override bool CanRead => this.innerStream.CanRead;
public override bool CanSeek => this.innerStream.CanSeek;
public override bool CanWrite => this.innerStream.CanWrite;
public override long Length => this.Await(() => this.innerStream.Length);
public override long Position { get => this.Await(() => this.innerStream.Position); set => this.Await(() => this.innerStream.Position = value); }
public override void Flush() => this.Await(() => this.innerStream.Flush());
public override int Read(byte[] buffer, int offset, int count) => this.Await(() => this.innerStream.Read(buffer, offset, count));
public override long Seek(long offset, SeekOrigin origin) => this.Await(() => this.innerStream.Seek(offset, origin));
public override void SetLength(long value) => this.Await(() => this.innerStream.SetLength(value));
public override void Write(byte[] buffer, int offset, int count) => this.Await(() => this.innerStream.Write(buffer, offset, count));
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => this.Await(() => this.innerStream.ReadAsync(buffer, offset, count, cancellationToken));
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => this.Await(() => this.innerStream.WriteAsync(buffer, offset, count, cancellationToken));
public override void WriteByte(byte value) => this.Await(() => this.innerStream.WriteByte(value));
public override int ReadByte() => this.Await(() => this.innerStream.ReadByte());
protected override void Dispose(bool disposing) => this.innerStream.Dispose();
#if NETCOREAPP
public override void CopyTo(Stream destination, int bufferSize) => this.Await(() => this.innerStream.CopyTo(destination, bufferSize));
public override int Read(Span<byte> buffer)
{
this.Wait();
return this.innerStream.Read(buffer);
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) => this.Await(() => this.innerStream.ReadAsync(buffer, cancellationToken));
public override void Write(ReadOnlySpan<byte> buffer)
{
this.Wait();
this.innerStream.Write(buffer);
}
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) => this.Await(() => this.innerStream.WriteAsync(buffer, cancellationToken));
#endif
}
}

9
tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs

@ -25,10 +25,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs
{
}
public MagickReferenceDecoder(bool validate)
{
this.validate = validate;
}
public MagickReferenceDecoder(bool validate) => this.validate = validate;
public static MagickReferenceDecoder Instance { get; } = new MagickReferenceDecoder();
@ -87,13 +84,13 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs
MemoryGroup<TPixel> framePixels = frame.PixelBuffer.FastMemoryGroup;
using IUnsafePixelCollection<ushort> pixels = magicFrame.GetPixelsUnsafe();
if (magicFrame.Depth == 8 || magicFrame.Depth == 1)
if (magicFrame.Depth == 8 || magicFrame.Depth == 4 || magicFrame.Depth == 2 || magicFrame.Depth == 1 || magicFrame.Depth == 10)
{
byte[] data = pixels.ToByteArray(PixelMapping.RGBA);
FromRgba32Bytes(configuration, data, framePixels);
}
else if (magicFrame.Depth == 16)
else if (magicFrame.Depth == 16 || magicFrame.Depth == 14)
{
ushort[] data = pixels.ToShortArray(PixelMapping.RGBA);
Span<byte> bytes = MemoryMarshal.Cast<ushort, byte>(data.AsSpan());

19
tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.IO;
using SixLabors.ImageSharp.IO;
namespace SixLabors.ImageSharp.Tests.TestUtilities
{
internal class SingleStreamFileSystem : IFileSystem
{
private readonly Stream stream;
public SingleStreamFileSystem(Stream stream) => this.stream = stream;
Stream IFileSystem.Create(string path) => this.stream;
Stream IFileSystem.OpenRead(string path) => this.stream;
}
}

3
tests/Images/Input/Tiff/flower-minisblack-04.tiff

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

3
tests/Images/Input/Tiff/flower-palette-04.tiff

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

3
tests/Images/Input/Tiff/flower-rgb-contig-02.tiff

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

3
tests/Images/Input/Tiff/flower-rgb-contig-04.tiff

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

3
tests/Images/Input/Tiff/flower-rgb-contig-10.tiff

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

3
tests/Images/Input/Tiff/flower-rgb-contig-14.tiff

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

3
tests/Images/Input/Tiff/flower-rgb-planar-02.tiff

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

3
tests/Images/Input/Tiff/flower-rgb-planar-04.tiff

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

3
tests/Images/Input/Tiff/flower-rgb-planar-10.tiff

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

3
tests/Images/Input/Tiff/flower-rgb-planar-14.tiff

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d28f021d40f53a011053f9644400fee2d29c02f97b4101fec899251125dbb18e
size 16855
Loading…
Cancel
Save