diff --git a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs
index be1d00e7e..9a4e4ba2b 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs
@@ -67,6 +67,11 @@ internal static class HorizontalPredictor
{
ApplyHorizontalPrediction8Bit(rows, width);
}
+ else if (bitsPerPixel == 16)
+ {
+ // Assume rows are L16 grayscale since that's currently the only way 16 bits is supported by encoder
+ ApplyHorizontalPrediction16Bit(rows, width);
+ }
else if (bitsPerPixel == 24)
{
ApplyHorizontalPrediction24Bit(rows, width);
@@ -102,6 +107,32 @@ internal static class HorizontalPredictor
}
}
+ ///
+ /// Applies a horizontal predictor to the L16 row.
+ /// Make use of the fact that many continuous-tone images rarely vary much in pixel value from one pixel to the next.
+ /// In such images, if we replace the pixel values by differences between consecutive pixels, many of the differences should be 0, plus
+ /// or minus 1, and so on.This reduces the apparent information content and allows LZW to encode the data more compactly.
+ ///
+ /// The L16 pixel rows.
+ /// The width.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static void ApplyHorizontalPrediction16Bit(Span rows, int width)
+ {
+ DebugGuard.IsTrue(rows.Length % width == 0, "Values must be equals");
+ int height = rows.Length / width;
+ for (int y = 0; y < height; y++)
+ {
+ Span rowSpan = rows.Slice(y * width, width);
+ Span rowL16 = MemoryMarshal.Cast(rowSpan);
+
+ for (int x = rowL16.Length - 1; x >= 1; x--)
+ {
+ ushort val = (ushort)(rowL16[x].PackedValue - rowL16[x - 1].PackedValue);
+ rowL16[x].PackedValue = val;
+ }
+ }
+ }
+
///
/// Applies a horizontal predictor to a gray pixel row.
///
diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs
index 05dacfef6..978860910 100644
--- a/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs
+++ b/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs
@@ -73,6 +73,11 @@ internal static class TiffConstants
///
public static readonly TiffBitsPerSample BitsPerSample8Bit = new TiffBitsPerSample(8, 0, 0);
+ ///
+ /// The bits per sample for 16-bit grayscale images.
+ ///
+ public static readonly TiffBitsPerSample BitsPerSample16Bit = new TiffBitsPerSample(16, 0, 0);
+
///
/// The bits per sample for color images with 8 bits for each color channel.
///
diff --git a/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs b/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs
index bcc249bbe..38da4b5f8 100644
--- a/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffBitsPerPixel.cs
@@ -54,7 +54,7 @@ public enum TiffBitsPerPixel
///
/// 16 bits per pixel, for gray images.
///
- /// Note: The TiffEncoder does not yet support 16 bits per color channel and will default to 24 bits per pixel instead.
+ /// Note: The TiffEncoder does not yet support 16 bits per color channel and will default to 16 bits grayscale instead.
///
Bit16 = 16,
diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
index 3a4e71d3e..94c7fb2b1 100644
--- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs
@@ -376,11 +376,14 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
case TiffBitsPerPixel.Bit8:
this.SetEncoderOptions(bitsPerPixel, photometricInterpretation ?? TiffPhotometricInterpretation.BlackIsZero, compression, predictor);
break;
+ case TiffBitsPerPixel.Bit16:
+ // Assume desire to encode as L16 grayscale
+ this.SetEncoderOptions(bitsPerPixel, TiffPhotometricInterpretation.BlackIsZero, compression, predictor);
+ break;
case TiffBitsPerPixel.Bit6:
case TiffBitsPerPixel.Bit10:
case TiffBitsPerPixel.Bit12:
case TiffBitsPerPixel.Bit14:
- case TiffBitsPerPixel.Bit16:
case TiffBitsPerPixel.Bit30:
case TiffBitsPerPixel.Bit36:
case TiffBitsPerPixel.Bit42:
@@ -413,13 +416,20 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
return;
}
- // At the moment only 8 and 32 bits per pixel can be preserved by the tiff encoder.
+ // At the moment only 8, 16 and 32 bits per pixel can be preserved by the tiff encoder.
if (inputBitsPerPixel == 8)
{
this.SetEncoderOptions(TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.BlackIsZero, compression, predictor);
return;
}
+ if (inputBitsPerPixel == 16)
+ {
+ // Assume desire to encode as L16 grayscale
+ this.SetEncoderOptions(TiffBitsPerPixel.Bit16, TiffPhotometricInterpretation.BlackIsZero, compression, predictor);
+ return;
+ }
+
this.SetEncoderOptions(TiffBitsPerPixel.Bit24, TiffPhotometricInterpretation.Rgb, compression, predictor);
return;
}
@@ -434,6 +444,12 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals
return;
}
+ if (inputBitsPerPixel == 16)
+ {
+ this.SetEncoderOptions(TiffBitsPerPixel.Bit16, photometricInterpretation, compression, predictor);
+ return;
+ }
+
this.SetEncoderOptions(TiffBitsPerPixel.Bit8, photometricInterpretation, compression, predictor);
return;
diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
index 7dd073936..cf9b4ae21 100644
--- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
@@ -343,20 +343,20 @@ internal class TiffEncoderEntriesCollector
return TiffConstants.BitsPerSampleRgb8Bit.ToArray();
case TiffPhotometricInterpretation.WhiteIsZero:
- if (encoder.BitsPerPixel == TiffBitsPerPixel.Bit1)
+ return encoder.BitsPerPixel switch
{
- return TiffConstants.BitsPerSample1Bit.ToArray();
- }
-
- return TiffConstants.BitsPerSample8Bit.ToArray();
+ TiffBitsPerPixel.Bit1 => TiffConstants.BitsPerSample1Bit.ToArray(),
+ TiffBitsPerPixel.Bit16 => TiffConstants.BitsPerSample16Bit.ToArray(),
+ _ => TiffConstants.BitsPerSample8Bit.ToArray()
+ };
case TiffPhotometricInterpretation.BlackIsZero:
- if (encoder.BitsPerPixel == TiffBitsPerPixel.Bit1)
+ return encoder.BitsPerPixel switch
{
- return TiffConstants.BitsPerSample1Bit.ToArray();
- }
-
- return TiffConstants.BitsPerSample8Bit.ToArray();
+ TiffBitsPerPixel.Bit1 => TiffConstants.BitsPerSample1Bit.ToArray(),
+ TiffBitsPerPixel.Bit16 => TiffConstants.BitsPerSample16Bit.ToArray(),
+ _ => TiffConstants.BitsPerSample8Bit.ToArray()
+ };
default:
return TiffConstants.BitsPerSampleRgb8Bit.ToArray();
diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs
index a52d49a35..96c8aeb32 100644
--- a/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs
+++ b/src/ImageSharp/Formats/Tiff/Writers/TiffColorWriterFactory.cs
@@ -27,12 +27,13 @@ internal static class TiffColorWriterFactory
return new TiffPaletteWriter(image, quantizer, pixelSamplingStrategy, memoryAllocator, configuration, entriesCollector, bitsPerPixel);
case TiffPhotometricInterpretation.BlackIsZero:
case TiffPhotometricInterpretation.WhiteIsZero:
- if (bitsPerPixel == 1)
+ return bitsPerPixel switch
{
- return new TiffBiColorWriter(image, memoryAllocator, configuration, entriesCollector);
- }
+ 1 => new TiffBiColorWriter(image, memoryAllocator, configuration, entriesCollector),
+ 16 => new TiffGrayL16Writer(image, memoryAllocator, configuration, entriesCollector),
+ _ => new TiffGrayWriter(image, memoryAllocator, configuration, entriesCollector)
+ };
- return new TiffGrayWriter(image, memoryAllocator, configuration, entriesCollector);
default:
return new TiffRgbWriter(image, memoryAllocator, configuration, entriesCollector);
}
diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs
new file mode 100644
index 000000000..3e0e074e9
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/Writers/TiffGrayL16Writer{TPixel}.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Tiff.Writers;
+
+internal sealed class TiffGrayL16Writer : TiffCompositeColorWriter
+ where TPixel : unmanaged, IPixel
+{
+ public TiffGrayL16Writer(ImageFrame image, MemoryAllocator memoryAllocator, Configuration configuration, TiffEncoderEntriesCollector entriesCollector)
+ : base(image, memoryAllocator, configuration, entriesCollector)
+ {
+ }
+
+ ///
+ public override int BitsPerPixel => 16;
+
+ ///
+ protected override void EncodePixels(Span pixels, Span buffer) => PixelOperations.Instance.ToL16Bytes(this.Configuration, pixels, buffer, pixels.Length);
+}
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
index 97445ad6c..bae429e65 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
@@ -17,6 +17,7 @@ public class TiffEncoderTests : TiffEncoderBaseTester
[InlineData(TiffPhotometricInterpretation.PaletteColor, TiffBitsPerPixel.Bit8)]
[InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffBitsPerPixel.Bit8)]
[InlineData(TiffPhotometricInterpretation.WhiteIsZero, TiffBitsPerPixel.Bit8)]
+ [InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffBitsPerPixel.Bit16)]
//// Unsupported TiffPhotometricInterpretation should default to 24 bits
[InlineData(TiffPhotometricInterpretation.CieLab, TiffBitsPerPixel.Bit24)]
[InlineData(TiffPhotometricInterpretation.ColorFilterArray, TiffBitsPerPixel.Bit24)]
@@ -28,7 +29,9 @@ public class TiffEncoderTests : TiffEncoderBaseTester
{
// arrange
var tiffEncoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation };
- using Image input = new Image(10, 10);
+ using Image input = expectedBitsPerPixel is TiffBitsPerPixel.Bit16
+ ? new Image(10, 10)
+ : new Image(10, 10);
using var memStream = new MemoryStream();
// act
@@ -44,6 +47,7 @@ public class TiffEncoderTests : TiffEncoderBaseTester
[Theory]
[InlineData(TiffBitsPerPixel.Bit24)]
+ [InlineData(TiffBitsPerPixel.Bit16)]
[InlineData(TiffBitsPerPixel.Bit8)]
[InlineData(TiffBitsPerPixel.Bit4)]
[InlineData(TiffBitsPerPixel.Bit1)]
@@ -117,14 +121,17 @@ public class TiffEncoderTests : TiffEncoderBaseTester
[Theory]
[InlineData(null, TiffCompression.Deflate, TiffBitsPerPixel.Bit24, TiffCompression.Deflate)]
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.Deflate, TiffBitsPerPixel.Bit24, TiffCompression.Deflate)]
+ [InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Deflate, TiffBitsPerPixel.Bit16, TiffCompression.Deflate)]
[InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Deflate, TiffBitsPerPixel.Bit8, TiffCompression.Deflate)]
[InlineData(TiffPhotometricInterpretation.PaletteColor, TiffCompression.Deflate, TiffBitsPerPixel.Bit8, TiffCompression.Deflate)]
[InlineData(null, TiffCompression.PackBits, TiffBitsPerPixel.Bit24, TiffCompression.PackBits)]
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.PackBits, TiffBitsPerPixel.Bit24, TiffCompression.PackBits)]
- [InlineData(TiffPhotometricInterpretation.PaletteColor, TiffCompression.PackBits, TiffBitsPerPixel.Bit8, TiffCompression.PackBits)]
+ [InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.PackBits, TiffBitsPerPixel.Bit16, TiffCompression.PackBits)]
[InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.PackBits, TiffBitsPerPixel.Bit8, TiffCompression.PackBits)]
+ [InlineData(TiffPhotometricInterpretation.PaletteColor, TiffCompression.PackBits, TiffBitsPerPixel.Bit8, TiffCompression.PackBits)]
[InlineData(null, TiffCompression.Lzw, TiffBitsPerPixel.Bit24, TiffCompression.Lzw)]
[InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.Lzw, TiffBitsPerPixel.Bit24, TiffCompression.Lzw)]
+ [InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Lzw, TiffBitsPerPixel.Bit16, TiffCompression.Lzw)]
[InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Lzw, TiffBitsPerPixel.Bit8, TiffCompression.Lzw)]
[InlineData(TiffPhotometricInterpretation.PaletteColor, TiffCompression.Lzw, TiffBitsPerPixel.Bit8, TiffCompression.Lzw)]
[InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup3Fax, TiffBitsPerPixel.Bit1, TiffCompression.CcittGroup3Fax)]
@@ -143,7 +150,9 @@ public class TiffEncoderTests : TiffEncoderBaseTester
{
// arrange
var tiffEncoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation, Compression = compression };
- using Image input = new Image(10, 10);
+ using Image input = expectedBitsPerPixel is TiffBitsPerPixel.Bit16
+ ? new Image(10, 10)
+ : new Image(10, 10);
using var memStream = new MemoryStream();
// act
@@ -160,6 +169,7 @@ public class TiffEncoderTests : TiffEncoderBaseTester
[Theory]
[WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32, TiffBitsPerPixel.Bit1)]
[WithFile(GrayscaleUncompressed, PixelTypes.Rgba32, TiffBitsPerPixel.Bit8)]
+ [WithFile(GrayscaleUncompressed, PixelTypes.L16, TiffBitsPerPixel.Bit16)]
[WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffBitsPerPixel.Bit24)]
[WithFile(Rgb4BitPalette, PixelTypes.Rgba32, TiffBitsPerPixel.Bit4)]
[WithFile(RgbPalette, PixelTypes.Rgba32, TiffBitsPerPixel.Bit8)]
@@ -406,6 +416,36 @@ public class TiffEncoderTests : TiffEncoderBaseTester
where TPixel : unmanaged, IPixel =>
TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit8, TiffPhotometricInterpretation.PaletteColor, TiffCompression.Lzw, TiffPredictor.Horizontal, useExactComparer: false, compareTolerance: 0.001f);
+ [Theory]
+ [WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)]
+ public void TiffEncoder_EncodeGray16_Works(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit16, TiffPhotometricInterpretation.BlackIsZero);
+
+ [Theory]
+ [WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)]
+ public void TiffEncoder_EncodeGray16_WithDeflateCompression_Works(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit16, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Deflate);
+
+ [Theory]
+ [WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)]
+ public void TiffEncoder_EncodeGray16_WithDeflateCompressionAndPredictor_Works(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit16, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Deflate, TiffPredictor.Horizontal);
+
+ [Theory]
+ [WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)]
+ public void TiffEncoder_EncodeGray16_WithLzwCompression_Works(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit16, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Lzw);
+
+ [Theory]
+ [WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)]
+ public void TiffEncoder_EncodeGray16_WithLzwCompressionAndPredictor_Works(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit16, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Lzw, TiffPredictor.Horizontal);
+
+ [Theory]
+ [WithFile(Calliphora_GrayscaleUncompressed, PixelTypes.Rgba32)]
+ public void TiffEncoder_EncodeGray16_WithPackBitsCompression_Works(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit16, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.PackBits);
+
[Theory]
[WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeBiColor_BlackIsZero_Works(TestImageProvider provider)
@@ -473,6 +513,7 @@ public class TiffEncoderTests : TiffEncoderBaseTester
[Theory]
[WithFile(GrayscaleUncompressed, PixelTypes.L8, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.PackBits)]
+ [WithFile(GrayscaleUncompressed, PixelTypes.L16, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.PackBits)]
[WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffPhotometricInterpretation.Rgb, TiffCompression.Deflate)]
[WithFile(RgbUncompressed, PixelTypes.Rgb24, TiffPhotometricInterpretation.Rgb, TiffCompression.None)]
[WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffPhotometricInterpretation.Rgb, TiffCompression.None)]