diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs
index a30890a69e..6fe412b925 100644
--- a/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs
+++ b/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs
@@ -96,10 +96,30 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Constants
public static readonly ushort[] BitsPerSample8Bit = { 8 };
///
- /// 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.
+ ///
+ public static readonly ushort[] BitsPerSampleRgb2Bit = { 2, 2, 2 };
+
+ ///
+ /// The bits per sample for color images with 4 bits for each color channel.
+ ///
+ public static readonly ushort[] BitsPerSampleRgb4Bit = { 4, 4, 4 };
+
+ ///
+ /// The bits per sample for color images with 8 bits for each color channel.
///
public static readonly ushort[] BitsPerSampleRgb8Bit = { 8, 8, 8 };
+ ///
+ /// The bits per sample for color images with 10 bits for each color channel.
+ ///
+ public static readonly ushort[] BitsPerSampleRgb10Bit = { 10, 10, 10 };
+
+ ///
+ /// The bits per sample for color images with 14 bits for each color channel.
+ ///
+ public static readonly ushort[] BitsPerSampleRgb14Bit = { 14, 14, 14 };
+
///
/// The list of mimetypes that equate to a tiff.
///
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb444TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb444TiffColor{TPixel}.cs
new file mode 100644
index 0000000000..d8c48942f8
--- /dev/null
+++ b/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
+{
+ ///
+ /// Implements the 'RGB' photometric interpretation for 4 bits per color channel images.
+ ///
+ internal class Rgb444TiffColor : TiffBaseColorDecoder
+ where TPixel : unmanaged, IPixel
+ {
+ ///
+ public override void Decode(ReadOnlySpan data, Buffer2D 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 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));
+ }
+}
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs
index e45dd44bdc..b40158fcee 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs
+++ b/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];
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
index 0a7941dfbc..5555eb537c 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
+++ b/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(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(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();
+
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();
+ 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(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(bitsPerSample);
+
case TiffColorType.PaletteColor:
DebugGuard.NotNull(bitsPerSample, "bitsPerSample");
DebugGuard.NotNull(colorMap, "colorMap");
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs
index 484d231633..22d8199533 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs
@@ -58,11 +58,31 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
///
Rgb,
+ ///
+ /// RGB color image with 2 bits for each channel.
+ ///
+ Rgb222,
+
+ ///
+ /// RGB color image with 4 bits for each channel.
+ ///
+ Rgb444,
+
///
/// RGB Full Color. Optimized implementation for 8-bit images.
///
Rgb888,
+ ///
+ /// RGB color image with 10 bits for each channel.
+ ///
+ Rgb101010,
+
+ ///
+ /// RGB color image with 14 bits for each channel.
+ ///
+ Rgb141414,
+
///
/// RGB Full Color. Planar configuration of data.
///
diff --git a/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs b/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs
index 35a9a590bb..ab9f3cbec0 100644
--- a/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs
@@ -18,14 +18,42 @@ namespace SixLabors.ImageSharp.Formats.Tiff
///
Bit4 = 4,
+ ///
+ /// 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.
+ ///
+ Bit6 = 6,
+
///
/// 8 bits per pixel, grayscale or color palette images.
///
Bit8 = 8,
+ ///
+ /// 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.
+ ///
+ Bit12 = 12,
+
///
/// 24 bits per pixel. One byte for each color channel.
///
Bit24 = 24,
+
+ ///
+ /// 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.
+ ///
+ Bit30 = 30,
+
+ ///
+ /// 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.
+ ///
+ Bit42 = 42,
}
}
diff --git a/src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs b/src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs
index bc74cbc5fb..088ef5d6f8 100644
--- a/src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs
@@ -28,9 +28,29 @@ namespace SixLabors.ImageSharp.Formats.Tiff
///
Bit8 = 8,
+ ///
+ /// Six bits per sample, each channel has 2 bits.
+ ///
+ Bit6 = 6,
+
+ ///
+ /// Twelve bits per sample, each channel has 4 bits.
+ ///
+ Bit12 = 12,
+
///
/// 24 bits per sample, each color channel has 8 Bits.
///
Bit24 = 24,
+
+ ///
+ /// Thirty bits per sample, each channel has 10 bits.
+ ///
+ Bit30 = 30,
+
+ ///
+ /// Forty two bits per sample, each channel has 14 bits.
+ ///
+ Bit42 = 42,
}
}
diff --git a/src/ImageSharp/Formats/Tiff/TiffBitsPerSampleExtensions.cs b/src/ImageSharp/Formats/Tiff/TiffBitsPerSampleExtensions.cs
index 5c4c374bef..ca0f0befcc 100644
--- a/src/ImageSharp/Formats/Tiff/TiffBitsPerSampleExtensions.cs
+++ b/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();
@@ -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:
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
index b7b7640072..fadb4f7c2e 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
+++ b/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;
}
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
index b5f3e7cf1e..eeac6a33c2 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
+++ b/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"),
};
}
}
diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
index 74c516f63b..d5137c4357 100644
--- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
+++ b/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;
diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
index 09605bc690..9bc0792c40 100644
--- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
+++ b/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);
diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs
index 712578f81a..d1a3dd1ea3 100644
--- a/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs
@@ -55,23 +55,38 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers
///
protected override void EncodeStrip(int y, int height, TiffBaseCompressor compressor)
{
- Span pixels = GetStripPixels(((IPixelSource)this.quantizedImage).PixelBuffer, y, height);
+ Span indexedPixels = GetStripPixels(((IPixelSource)this.quantizedImage).PixelBuffer, y, height);
if (this.BitsPerPixel == 4)
{
- using IMemoryOwner rows4bitBuffer = this.MemoryAllocator.Allocate(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 rows4bitBuffer = this.MemoryAllocator.Allocate(rows4BitBufferLength);
Span 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.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.
diff --git a/src/ImageSharp/Image.cs b/src/ImageSharp/Image.cs
index fbb3ec2069..ce6aa69b58 100644
--- a/src/ImageSharp/Image.cs
+++ b/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
///
public abstract partial class Image : IImage, IConfigurationProvider
{
+ private bool isDisposed;
+
private Size size;
private readonly Configuration configuration;
@@ -80,8 +83,15 @@ namespace SixLabors.ImageSharp
///
public void Dispose()
{
+ if (this.isDisposed)
+ {
+ return;
+ }
+
this.Dispose(true);
GC.SuppressFinalize(this);
+
+ this.isDisposed = true;
}
///
@@ -89,7 +99,7 @@ namespace SixLabors.ImageSharp
///
/// The stream to save the image to.
/// The encoder to save the image with.
- /// Thrown if the stream or encoder is null.
+ /// Thrown if the stream or encoder is null.
public void Save(Stream stream, IImageEncoder encoder)
{
Guard.NotNull(stream, nameof(stream));
@@ -148,7 +158,13 @@ namespace SixLabors.ImageSharp
///
/// Throws if the image is disposed.
///
- internal abstract void EnsureNotDisposed();
+ internal void EnsureNotDisposed()
+ {
+ if (this.isDisposed)
+ {
+ ThrowObjectDisposedException(this.GetType());
+ }
+ }
///
/// Accepts a .
@@ -167,6 +183,9 @@ namespace SixLabors.ImageSharp
/// The token to monitor for cancellation requests.
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;
diff --git a/src/ImageSharp/ImageFrameCollection.cs b/src/ImageSharp/ImageFrameCollection.cs
index 62ecc71f55..07ba8c87f3 100644
--- a/src/ImageSharp/ImageFrameCollection.cs
+++ b/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 instances
/// that make up an .
///
- public abstract class ImageFrameCollection : IEnumerable
+ public abstract class ImageFrameCollection : IDisposable, IEnumerable
{
+ private bool isDisposed;
+
///
/// Gets the number of frames.
///
@@ -21,7 +24,15 @@ namespace SixLabors.ImageSharp
///
/// Gets the root frame.
///
- public ImageFrame RootFrame => this.NonGenericRootFrame;
+ public ImageFrame RootFrame
+ {
+ get
+ {
+ this.EnsureNotDisposed();
+
+ return this.NonGenericRootFrame;
+ }
+ }
///
/// Gets the root frame. (Implements .)
@@ -36,7 +47,15 @@ namespace SixLabors.ImageSharp
///
/// The index.
/// The at the specified index.
- public ImageFrame this[int index] => this.NonGenericGetFrame(index);
+ public ImageFrame this[int index]
+ {
+ get
+ {
+ this.EnsureNotDisposed();
+
+ return this.NonGenericGetFrame(index);
+ }
+ }
///
/// Determines the index of a specific in the .
@@ -52,14 +71,24 @@ namespace SixLabors.ImageSharp
/// The to clone and insert into the .
/// Frame must have the same dimensions as the image.
/// The cloned .
- 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);
+ }
///
/// Clones the frame and appends the clone to the end of the collection.
///
/// The raw pixel data to generate the from.
/// The cloned .
- public ImageFrame AddFrame(ImageFrame source) => this.NonGenericAddFrame(source);
+ public ImageFrame AddFrame(ImageFrame source)
+ {
+ this.EnsureNotDisposed();
+
+ return this.NonGenericAddFrame(source);
+ }
///
/// Removes the frame at the specified index and frees all freeable resources associated with it.
@@ -91,7 +120,12 @@ namespace SixLabors.ImageSharp
/// The zero-based index of the frame to export.
/// Cannot remove last frame.
/// The new with the specified frame.
- public Image ExportFrame(int index) => this.NonGenericExportFrame(index);
+ public Image ExportFrame(int index)
+ {
+ this.EnsureNotDisposed();
+
+ return this.NonGenericExportFrame(index);
+ }
///
/// Creates an with only the frame at the specified index
@@ -99,7 +133,12 @@ namespace SixLabors.ImageSharp
///
/// The zero-based index of the frame to clone.
/// The new with the specified frame.
- public Image CloneFrame(int index) => this.NonGenericCloneFrame(index);
+ public Image CloneFrame(int index)
+ {
+ this.EnsureNotDisposed();
+
+ return this.NonGenericCloneFrame(index);
+ }
///
/// Creates a new and appends it to the end of the collection.
@@ -107,7 +146,12 @@ namespace SixLabors.ImageSharp
///
/// The new .
///
- public ImageFrame CreateFrame() => this.NonGenericCreateFrame();
+ public ImageFrame CreateFrame()
+ {
+ this.EnsureNotDisposed();
+
+ return this.NonGenericCreateFrame();
+ }
///
/// Creates a new and appends it to the end of the collection.
@@ -116,14 +160,55 @@ namespace SixLabors.ImageSharp
///
/// The new .
///
- public ImageFrame CreateFrame(Color backgroundColor) => this.NonGenericCreateFrame(backgroundColor);
+ public ImageFrame CreateFrame(Color backgroundColor)
+ {
+ this.EnsureNotDisposed();
+
+ return this.NonGenericCreateFrame(backgroundColor);
+ }
///
- public IEnumerator GetEnumerator() => this.NonGenericGetEnumerator();
+ public void Dispose()
+ {
+ if (this.isDisposed)
+ {
+ return;
+ }
+
+ this.Dispose(true);
+ GC.SuppressFinalize(this);
+
+ this.isDisposed = true;
+ }
+
+ ///
+ public IEnumerator GetEnumerator()
+ {
+ this.EnsureNotDisposed();
+
+ return this.NonGenericGetEnumerator();
+ }
///
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
+ ///
+ /// Throws if the image frame is disposed.
+ ///
+ protected void EnsureNotDisposed()
+ {
+ if (this.isDisposed)
+ {
+ ThrowObjectDisposedException(this.GetType());
+ }
+ }
+
+ ///
+ /// Disposes the object and frees resources for the Garbage Collector.
+ ///
+ /// Whether to dispose of managed and unmanaged objects.
+ protected abstract void Dispose(bool disposing);
+
///
/// Implements .
///
@@ -178,5 +263,8 @@ namespace SixLabors.ImageSharp
/// The background color.
/// The new frame.
protected abstract ImageFrame NonGenericCreateFrame(Color backgroundColor);
+
+ [MethodImpl(InliningOptions.ColdPath)]
+ private static void ThrowObjectDisposedException(Type type) => throw new ObjectDisposedException(type.Name);
}
}
diff --git a/src/ImageSharp/ImageFrameCollection{TPixel}.cs b/src/ImageSharp/ImageFrameCollection{TPixel}.cs
index 36c3ee481f..da024c9176 100644
--- a/src/ImageSharp/ImageFrameCollection{TPixel}.cs
+++ b/src/ImageSharp/ImageFrameCollection{TPixel}.cs
@@ -67,7 +67,26 @@ namespace SixLabors.ImageSharp
///
/// Gets the root frame.
///
- public new ImageFrame RootFrame => this.frames.Count > 0 ? this.frames[0] : null;
+ public new ImageFrame 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];
+ }
+ }
+
+ ///
+ /// Gets root frame accessor in unsafe manner without any checks.
+ ///
+ ///
+ /// This property is most likely to be called from for indexing pixels.
+ /// already checks if it was disposed before querying for root frame.
+ ///
+ internal ImageFrame RootFrameUnsafe => this.frames[0];
///
protected override ImageFrame NonGenericRootFrame => this.RootFrame;
@@ -80,12 +99,22 @@ namespace SixLabors.ImageSharp
///
/// The index.
/// The at the specified index.
- public new ImageFrame this[int index] => this.frames[index];
+ public new ImageFrame this[int index]
+ {
+ get
+ {
+ this.EnsureNotDisposed();
+
+ return this.frames[index];
+ }
+ }
///
public override int IndexOf(ImageFrame frame)
{
- return frame is ImageFrame specific ? this.IndexOf(specific) : -1;
+ this.EnsureNotDisposed();
+
+ return frame is ImageFrame specific ? this.frames.IndexOf(specific) : -1;
}
///
@@ -93,7 +122,12 @@ namespace SixLabors.ImageSharp
///
/// The to locate in the .
/// The index of item if found in the list; otherwise, -1.
- public int IndexOf(ImageFrame frame) => this.frames.IndexOf(frame);
+ public int IndexOf(ImageFrame frame)
+ {
+ this.EnsureNotDisposed();
+
+ return this.frames.IndexOf(frame);
+ }
///
/// Clones and inserts the into the at the specified .
@@ -104,6 +138,8 @@ namespace SixLabors.ImageSharp
/// The cloned .
public ImageFrame InsertFrame(int index, ImageFrame source)
{
+ this.EnsureNotDisposed();
+
this.ValidateFrame(source);
ImageFrame clonedFrame = source.Clone(this.parent.GetConfiguration());
this.frames.Insert(index, clonedFrame);
@@ -117,6 +153,8 @@ namespace SixLabors.ImageSharp
/// The cloned .
public ImageFrame AddFrame(ImageFrame source)
{
+ this.EnsureNotDisposed();
+
this.ValidateFrame(source);
ImageFrame clonedFrame = source.Clone(this.parent.GetConfiguration());
this.frames.Add(clonedFrame);
@@ -131,6 +169,8 @@ namespace SixLabors.ImageSharp
/// The new .
public ImageFrame AddFrame(ReadOnlySpan source)
{
+ this.EnsureNotDisposed();
+
var frame = ImageFrame.LoadPixelData(
this.parent.GetConfiguration(),
source,
@@ -149,6 +189,7 @@ namespace SixLabors.ImageSharp
public ImageFrame AddFrame(TPixel[] source)
{
Guard.NotNull(source, nameof(source));
+
return this.AddFrame(source.AsSpan());
}
@@ -159,6 +200,8 @@ namespace SixLabors.ImageSharp
/// Cannot remove last frame.
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
}
///
- public override bool Contains(ImageFrame frame) =>
- frame is ImageFrame specific && this.Contains(specific);
+ public override bool Contains(ImageFrame frame)
+ {
+ this.EnsureNotDisposed();
+
+ return frame is ImageFrame specific && this.frames.Contains(specific);
+ }
///
/// Determines whether the contains the .
@@ -180,7 +227,12 @@ namespace SixLabors.ImageSharp
///
/// true if the contains the specified frame; otherwise, false.
///
- public bool Contains(ImageFrame frame) => this.frames.Contains(frame);
+ public bool Contains(ImageFrame frame)
+ {
+ this.EnsureNotDisposed();
+
+ return this.frames.Contains(frame);
+ }
///
/// Moves an from to .
@@ -189,6 +241,8 @@ namespace SixLabors.ImageSharp
/// The index to move the frame to.
public override void MoveFrame(int sourceIndex, int destinationIndex)
{
+ this.EnsureNotDisposed();
+
if (sourceIndex == destinationIndex)
{
return;
@@ -208,6 +262,8 @@ namespace SixLabors.ImageSharp
/// The new with the specified frame.
public new Image ExportFrame(int index)
{
+ this.EnsureNotDisposed();
+
ImageFrame frame = this[index];
if (this.Count == 1 && this.frames.Contains(frame))
@@ -228,6 +284,8 @@ namespace SixLabors.ImageSharp
/// The new with the specified frame.
public new Image CloneFrame(int index)
{
+ this.EnsureNotDisposed();
+
ImageFrame frame = this[index];
ImageFrame clonedFrame = frame.Clone();
return new Image(this.parent.GetConfiguration(), this.parent.Metadata.DeepClone(), new[] { clonedFrame });
@@ -241,6 +299,8 @@ namespace SixLabors.ImageSharp
///
public new ImageFrame CreateFrame()
{
+ this.EnsureNotDisposed();
+
var frame = new ImageFrame(
this.parent.GetConfiguration(),
this.RootFrame.Width,
@@ -335,14 +395,18 @@ namespace SixLabors.ImageSharp
}
}
- internal void Dispose()
+ ///
+ protected override void Dispose(bool disposing)
{
- foreach (ImageFrame f in this.frames)
+ if (disposing)
{
- f.Dispose();
- }
+ foreach (ImageFrame f in this.frames)
+ {
+ f.Dispose();
+ }
- this.frames.Clear();
+ this.frames.Clear();
+ }
}
private ImageFrame CopyNonCompatibleFrame(ImageFrame source)
diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs
index 83ecc37530..b43ff0422b 100644
--- a/src/ImageSharp/Image{TPixel}.cs
+++ b/src/ImageSharp/Image{TPixel}.cs
@@ -23,7 +23,7 @@ namespace SixLabors.ImageSharp
public sealed class Image : Image
where TPixel : unmanaged, IPixel
{
- private bool isDisposed;
+ private readonly ImageFrameCollection frames;
///
/// Initializes a new instance of the class
@@ -84,7 +84,7 @@ namespace SixLabors.ImageSharp
internal Image(Configuration configuration, int width, int height, ImageMetadata metadata)
: base(configuration, PixelTypeInfo.Create(), metadata, width, height)
{
- this.Frames = new ImageFrameCollection(this, width, height, default(TPixel));
+ this.frames = new ImageFrameCollection(this, width, height, default(TPixel));
}
///
@@ -104,7 +104,7 @@ namespace SixLabors.ImageSharp
ImageMetadata metadata)
: base(configuration, PixelTypeInfo.Create(), metadata, width, height)
{
- this.Frames = new ImageFrameCollection(this, width, height, memoryGroup);
+ this.frames = new ImageFrameCollection(this, width, height, memoryGroup);
}
///
@@ -124,7 +124,7 @@ namespace SixLabors.ImageSharp
ImageMetadata metadata)
: base(configuration, PixelTypeInfo.Create(), metadata, width, height)
{
- this.Frames = new ImageFrameCollection(this, width, height, backgroundColor);
+ this.frames = new ImageFrameCollection(this, width, height, backgroundColor);
}
///
@@ -137,7 +137,7 @@ namespace SixLabors.ImageSharp
internal Image(Configuration configuration, ImageMetadata metadata, IEnumerable> frames)
: base(configuration, PixelTypeInfo.Create(), metadata, ValidateFramesAndGetSize(frames))
{
- this.Frames = new ImageFrameCollection(this, frames);
+ this.frames = new ImageFrameCollection(this, frames);
}
///
@@ -146,12 +146,19 @@ namespace SixLabors.ImageSharp
///
/// Gets the collection of image frames.
///
- public new ImageFrameCollection Frames { get; }
+ public new ImageFrameCollection Frames
+ {
+ get
+ {
+ this.EnsureNotDisposed();
+ return this.frames;
+ }
+ }
///
/// Gets the root frame.
///
- private IPixelSource PixelSource => this.Frames?.RootFrame ?? throw new ObjectDisposedException(nameof(Image));
+ private IPixelSource PixelSourceUnsafe => this.frames.RootFrameUnsafe;
///
/// 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);
}
///
@@ -226,10 +239,10 @@ namespace SixLabors.ImageSharp
{
this.EnsureNotDisposed();
- var clonedFrames = new ImageFrame[this.Frames.Count];
+ var clonedFrames = new ImageFrame[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(configuration, this.Metadata.DeepClone(), clonedFrames);
@@ -245,10 +258,10 @@ namespace SixLabors.ImageSharp
{
this.EnsureNotDisposed();
- var clonedFrames = new ImageFrame[this.Frames.Count];
+ var clonedFrames = new ImageFrame[this.frames.Count];
for (int i = 0; i < clonedFrames.Length; i++)
{
- clonedFrames[i] = this.Frames[i].CloneAs(configuration);
+ clonedFrames[i] = this.frames[i].CloneAs(configuration);
}
return new Image(configuration, this.Metadata.DeepClone(), clonedFrames);
@@ -257,25 +270,9 @@ namespace SixLabors.ImageSharp
///
protected override void Dispose(bool disposing)
{
- if (this.isDisposed)
- {
- return;
- }
-
if (disposing)
{
- this.Frames.Dispose();
- }
-
- this.isDisposed = true;
- }
-
- ///
- 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 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());
diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor.cs
index 56593acb84..9b28a8fdd8 100644
--- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor.cs
+++ b/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;
///
/// 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
///
public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle)
- {
- return new AdaptiveHistogramEqualizationProcessor(
+ => new AdaptiveHistogramEqualizationProcessor(
configuration,
this.LuminanceLevels,
this.ClipHistogram,
@@ -43,6 +39,5 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
this.NumberOfTiles,
source,
sourceRectangle);
- }
}
}
diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs
index 14687426d0..91ed9f5de4 100644
--- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs
+++ b/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.
+ ///
+ /// Used for storing the minimum value for each CDF entry.
+ ///
private readonly Buffer2D cdfMinBuffer2D;
- // Used for storing the LUT for each CDF entry.
+ ///
+ /// Used for storing the LUT for each CDF entry.
+ ///
private readonly Buffer2D 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 cdfMinSpan = this.cdfMinBuffer2D.GetRowSpan(cdfY);
+ cdfMinSpan.Clear();
using IMemoryOwner histogramBuffer = this.allocator.Allocate(this.luminanceLevels);
Span histogram = histogramBuffer.GetSpan();
diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs
index 60686f4014..f93334beb0 100644
--- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs
+++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs
@@ -49,44 +49,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
///
/// The .
/// The .
- 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),
+ };
}
}
diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs
index 59df3058d9..9227cb0c01 100644
--- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs
+++ b/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);
}
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
index 27d70fd188..67df6a8814 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
+++ b/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(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(() => 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(async () => await Image.IdentifyAsync(config, "someFakeFile", cts.Token));
}
// DEBUG ONLY!
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
index 9a1d423a6d..3c48865c71 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
+++ b/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(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(() => image.SaveAsync(stream, encoder, cts.Token));
+ using var image = new Image(5000, 5000);
+ await Assert.ThrowsAsync(async () =>
+ {
+ var encoder = new JpegEncoder() { Subsample = subsample };
+ await image.SaveAsync(pausedStream, encoder, cts.Token);
+ });
}
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
index 7a704ee1dc..a047d06c9b 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
+++ b/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(TestImageProvider provider)
where TPixel : unmanaged, IPixel => TestTiffDecoder(provider);
+ [Theory]
+ [WithFile(Rgb4BitPalette, PixelTypes.Rgba32)]
+ [WithFile(Flower4BitPalette, PixelTypes.Rgba32)]
+ [WithFile(Flower4BitPaletteGray, PixelTypes.Rgba32)]
+ public void TiffDecoder_CanDecode_4Bit_WithPalette(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel => TestTiffDecoder(provider, ReferenceDecoder, useExactComparer: false, 0.01f);
+
+ [Theory]
+ [WithFile(FlowerRgb222Contiguous, PixelTypes.Rgba32)]
+ [WithFile(FlowerRgb222Planar, PixelTypes.Rgba32)]
+ public void TiffDecoder_CanDecode_6Bit(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel => TestTiffDecoder(provider);
+
+ [Theory]
+ [WithFile(FlowerRgb444Contiguous, PixelTypes.Rgba32)]
+ [WithFile(FlowerRgb444Planar, PixelTypes.Rgba32)]
+ public void TiffDecoder_CanDecode_12Bit(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel => TestTiffDecoder(provider);
+
+ [Theory]
+ [WithFile(FlowerRgb101010Contiguous, PixelTypes.Rgba32)]
+ [WithFile(FlowerRgb101010Planar, PixelTypes.Rgba32)]
+ public void TiffDecoder_CanDecode_30Bit(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel => TestTiffDecoder(provider);
+
+ [Theory]
+ [WithFile(FlowerRgb141414Contiguous, PixelTypes.Rgba32)]
+ [WithFile(FlowerRgb141414Planar, PixelTypes.Rgba32)]
+ public void TiffDecoder_CanDecode_42Bit(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel => 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(TestImageProvider provider)
+ private static void TestTiffDecoder(TestImageProvider provider, IImageDecoder referenceDecoder = null, bool useExactComparer = true, float compareTolerance = 0.001f)
where TPixel : unmanaged, IPixel
{
using Image image = provider.GetImage(TiffDecoder);
image.DebugSave(provider);
- image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder);
+ image.CompareToOriginal(
+ provider,
+ useExactComparer ? ImageComparer.Exact : ImageComparer.Tolerant(compareTolerance),
+ referenceDecoder ?? ReferenceDecoder);
}
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
index 546508ca54..7c386a6a9a 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
+++ b/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(10, 10);
+ using var memStream = new MemoryStream();
+
+ // act
+ input.Save(memStream, tiffEncoder);
+
+ // assert
+ memStream.Position = 0;
+ using var output = Image.Load(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(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, imageDecoder: new TiffDecoder());
+
[Theory]
[WithFile(Calliphora_RgbUncompressed, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeRgb_Works(TestImageProvider 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(TestImageProvider provider)
where TPixel : unmanaged, IPixel =>
- //// 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
{
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
index 3aded7b0e3..ab350f720e 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
+++ b/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);
}
}
}
diff --git a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs
index a00f190dbd..dbc5af536d 100644
--- a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs
+++ b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs
@@ -339,6 +339,48 @@ namespace SixLabors.ImageSharp.Tests
using var frame = new ImageFrame(Configuration.Default, 10, 10);
Assert.False(this.Image.Frames.Contains(frame));
}
+
+ [Fact]
+ public void DisposeCall_NoThrowIfCalledMultiple()
+ {
+ var image = new Image(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(Configuration.Default, 10, 10);
+ var frameCollection = image.Frames as ImageFrameCollection;
+
+ image.Dispose(); // this should invalidate underlying collection as well
+
+ Assert.Throws(() => { var prop = frameCollection.RootFrame; });
+ }
+
+ [Fact]
+ public void PublicMethods_ThrowIfDisposed()
+ {
+ var image = new Image(Configuration.Default, 10, 10);
+ var frameCollection = image.Frames as ImageFrameCollection;
+
+ image.Dispose(); // this should invalidate underlying collection as well
+
+ Assert.Throws(() => { var res = frameCollection.AddFrame(default); });
+ Assert.Throws(() => { var res = frameCollection.CloneFrame(default); });
+ Assert.Throws(() => { var res = frameCollection.Contains(default); });
+ Assert.Throws(() => { var res = frameCollection.CreateFrame(); });
+ Assert.Throws(() => { var res = frameCollection.CreateFrame(default); });
+ Assert.Throws(() => { var res = frameCollection.ExportFrame(default); });
+ Assert.Throws(() => { var res = frameCollection.GetEnumerator(); });
+ Assert.Throws(() => { var prop = frameCollection.IndexOf(default); });
+ Assert.Throws(() => { var prop = frameCollection.InsertFrame(default, default); });
+ Assert.Throws(() => { frameCollection.RemoveFrame(default); });
+ Assert.Throws(() => { frameCollection.MoveFrame(default, default); });
+ }
}
}
}
diff --git a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs
index 92109ed479..15838f6902 100644
--- a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs
+++ b/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(Configuration.Default, 10, 10);
+ var frameCollection = image.Frames;
+
+ image.Dispose(); // this should invalidate underlying collection as well
+
+ Assert.Throws(() => { var prop = frameCollection.RootFrame; });
+ }
+
+ [Fact]
+ public void PublicMethods_ThrowIfDisposed()
+ {
+ var image = new Image(Configuration.Default, 10, 10);
+ var frameCollection = image.Frames;
+ var rgba32Array = new Rgba32[0];
+
+ image.Dispose(); // this should invalidate underlying collection as well
+
+ Assert.Throws(() => { var res = frameCollection.AddFrame((ImageFrame)null); });
+ Assert.Throws(() => { var res = frameCollection.AddFrame(rgba32Array); });
+ Assert.Throws(() => { var res = frameCollection.AddFrame((ImageFrame)null); });
+ Assert.Throws(() => { var res = frameCollection.AddFrame(rgba32Array.AsSpan()); });
+ Assert.Throws(() => { var res = frameCollection.CloneFrame(default); });
+ Assert.Throws(() => { var res = frameCollection.Contains(default); });
+ Assert.Throws(() => { var res = frameCollection.CreateFrame(); });
+ Assert.Throws(() => { var res = frameCollection.CreateFrame(default); });
+ Assert.Throws(() => { var res = frameCollection.ExportFrame(default); });
+ Assert.Throws(() => { var res = frameCollection.GetEnumerator(); });
+ Assert.Throws(() => { var prop = frameCollection.IndexOf(default); });
+ Assert.Throws(() => { var prop = frameCollection.InsertFrame(default, default); });
+ Assert.Throws(() => { frameCollection.RemoveFrame(default); });
+ Assert.Throws(() => { frameCollection.MoveFrame(default, default); });
+ }
+
///
/// Integration test for end-to end API validation.
///
diff --git a/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs
index 825bd55e47..8bb121349f 100644
--- a/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs
+++ b/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(() =>
- image.SaveAsync(asyncStream, encoder, cts.Token));
+ var pausedStream = new PausedStream(asyncStream);
+ pausedStream.OnWaiting(s =>
+ {
+ cts.Cancel();
+ pausedStream.Release();
+ });
+
+ await Assert.ThrowsAsync(async () => await image.SaveAsync(pausedStream, encoder, cts.Token));
}
}
}
diff --git a/tests/ImageSharp.Tests/Image/ImageTests.cs b/tests/ImageSharp.Tests/Image/ImageTests.cs
index b7c6b3835a..1296f26c47 100644
--- a/tests/ImageSharp.Tests/Image/ImageTests.cs
+++ b/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(this.configuration, 10, 10);
+ image.Dispose();
+ image.Dispose();
+ }
+
+ [Fact]
+ public void NonPrivateProperties_ObjectDisposedException()
+ {
+ var image = new Image(this.configuration, 10, 10);
+ var genericImage = (Image)image;
+
+ image.Dispose();
+
+ // Image
+ Assert.Throws(() => { var prop = image.Frames; });
+
+ // Image
+ Assert.Throws(() => { var prop = genericImage.Frames; });
+ }
+
+ [Fact]
+ public void Save_ObjectDisposedException()
+ {
+ using var stream = new MemoryStream();
+ var image = new Image(this.configuration, 10, 10);
+ var encoder = new JpegEncoder();
+
+ image.Dispose();
+
+ // Image
+ Assert.Throws(() => 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(this.configuration, 10, 10);
+ var genericImage = (Image)image;
+
+ image.Dispose();
+
+ // Image
+ Assert.Throws(() => { var res = image.Clone(this.configuration); });
+ Assert.Throws(() => { var res = image.CloneAs(this.configuration); });
+ Assert.Throws(() => { var res = image.GetPixelRowSpan(default); });
+ Assert.Throws(() => { var res = image.TryGetSinglePixelSpan(out var _); });
+
+ // Image
+ Assert.Throws(() => { var res = genericImage.CloneAs(this.configuration); });
+ }
+ }
}
}
diff --git a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
index ab3a1d7603..85b7530247 100644
--- a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
+++ b/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
///
/// The pixel type of the image.
+ /// The test image provider.
[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(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ 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 image = provider.GetImage();
+ using Image referenceResult = image.Clone(ctx =>
+ {
+ ctx.HistogramEqualization(options);
+ ctx.Resize(image.Width / 4, image.Height / 4, KnownResamplers.Bicubic);
+ });
+
+ using Image processed = image.Clone(ctx =>
+ {
+ ctx.HistogramEqualization(options);
+ ctx.Resize(image.Width / 4, image.Height / 4, KnownResamplers.Bicubic);
+ });
+
+ ValidatorComparer.VerifySimilarity(referenceResult, processed);
+ }
}
}
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index 195693d6b8..c640221730 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/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";
diff --git a/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs b/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs
new file mode 100644
index 0000000000..4d3646301f
--- /dev/null
+++ b/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 onWaitingCallback;
+
+ public void OnWaiting(Action 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 action)
+ {
+ await Task.Yield();
+ this.Wait();
+ await action();
+ }
+
+ private async Task Await(Func> action)
+ {
+ await Task.Yield();
+ this.Wait();
+ return await action();
+ }
+
+ private T Await(Func 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 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 buffer)
+ {
+ this.Wait();
+ return this.innerStream.Read(buffer);
+ }
+
+ public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) => this.Await(() => this.innerStream.ReadAsync(buffer, cancellationToken));
+
+ public override void Write(ReadOnlySpan buffer)
+ {
+ this.Wait();
+ this.innerStream.Write(buffer);
+ }
+
+ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => this.Await(() => this.innerStream.WriteAsync(buffer, cancellationToken));
+#endif
+ }
+}
diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs
index 4e2866be1f..dffbeac497 100644
--- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs
+++ b/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 framePixels = frame.PixelBuffer.FastMemoryGroup;
using IUnsafePixelCollection 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 bytes = MemoryMarshal.Cast(data.AsSpan());
diff --git a/tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs b/tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs
new file mode 100644
index 0000000000..ddd1ec7506
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/tests/Images/Input/Tiff/flower-minisblack-04.tiff b/tests/Images/Input/Tiff/flower-minisblack-04.tiff
new file mode 100644
index 0000000000..e6d1e13360
--- /dev/null
+++ b/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
diff --git a/tests/Images/Input/Tiff/flower-palette-04.tiff b/tests/Images/Input/Tiff/flower-palette-04.tiff
new file mode 100644
index 0000000000..8594a0b00a
--- /dev/null
+++ b/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
diff --git a/tests/Images/Input/Tiff/flower-rgb-contig-02.tiff b/tests/Images/Input/Tiff/flower-rgb-contig-02.tiff
new file mode 100644
index 0000000000..a2d253dbd5
--- /dev/null
+++ b/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
diff --git a/tests/Images/Input/Tiff/flower-rgb-contig-04.tiff b/tests/Images/Input/Tiff/flower-rgb-contig-04.tiff
new file mode 100644
index 0000000000..d9a141f29a
--- /dev/null
+++ b/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
diff --git a/tests/Images/Input/Tiff/flower-rgb-contig-10.tiff b/tests/Images/Input/Tiff/flower-rgb-contig-10.tiff
new file mode 100644
index 0000000000..2b271c8004
--- /dev/null
+++ b/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
diff --git a/tests/Images/Input/Tiff/flower-rgb-contig-14.tiff b/tests/Images/Input/Tiff/flower-rgb-contig-14.tiff
new file mode 100644
index 0000000000..d4d6a9492d
--- /dev/null
+++ b/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
diff --git a/tests/Images/Input/Tiff/flower-rgb-planar-02.tiff b/tests/Images/Input/Tiff/flower-rgb-planar-02.tiff
new file mode 100644
index 0000000000..8b301a534d
--- /dev/null
+++ b/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
diff --git a/tests/Images/Input/Tiff/flower-rgb-planar-04.tiff b/tests/Images/Input/Tiff/flower-rgb-planar-04.tiff
new file mode 100644
index 0000000000..7a2270e486
--- /dev/null
+++ b/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
diff --git a/tests/Images/Input/Tiff/flower-rgb-planar-10.tiff b/tests/Images/Input/Tiff/flower-rgb-planar-10.tiff
new file mode 100644
index 0000000000..be0acd6465
--- /dev/null
+++ b/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
diff --git a/tests/Images/Input/Tiff/flower-rgb-planar-14.tiff b/tests/Images/Input/Tiff/flower-rgb-planar-14.tiff
new file mode 100644
index 0000000000..2d517268e9
--- /dev/null
+++ b/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