diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero1TiffColor.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero1TiffColor.cs
new file mode 100644
index 0000000000..1b9e194e19
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero1TiffColor.cs
@@ -0,0 +1,54 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageSharp.Formats.Tiff
+{
+ using System;
+ using System.Runtime.CompilerServices;
+ using ImageSharp;
+ using ImageSharp.PixelFormats;
+
+ ///
+ /// Implements the 'BlackIsZero' photometric interpretation (optimised for bilevel images).
+ ///
+ internal static class BlackIsZero1TiffColor
+ {
+ ///
+ /// Decodes pixel data using the current photometric interpretation.
+ ///
+ /// The pixel format.
+ /// The buffer to read image data from.
+ /// The image buffer to write pixels to.
+ /// The x-coordinate of the left-hand side of the image block.
+ /// The y-coordinate of the top of the image block.
+ /// The width of the image block.
+ /// The height of the image block.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Decode(byte[] data, PixelAccessor pixels, int left, int top, int width, int height)
+ where TPixel : struct, IPixel
+ {
+ TPixel color = default(TPixel);
+
+ uint offset = 0;
+
+ for (int y = top; y < top + height; y++)
+ {
+ for (int x = left; x < left + width; x += 8)
+ {
+ byte b = data[offset++];
+ int maxShift = Math.Min(left + width - x, 8);
+
+ for (int shift = 0; shift < maxShift; shift++)
+ {
+ int bit = (b >> (7 - shift)) & 1;
+ byte intensity = (bit == 1) ? (byte)255 : (byte)0;
+ color.PackFromBytes(intensity, intensity, intensity, 255);
+ pixels[x + shift, y] = color;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero4TiffColor.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero4TiffColor.cs
new file mode 100644
index 0000000000..b52e5e045a
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero4TiffColor.cs
@@ -0,0 +1,62 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageSharp.Formats.Tiff
+{
+ using System.Runtime.CompilerServices;
+ using ImageSharp;
+ using ImageSharp.PixelFormats;
+
+ ///
+ /// Implements the 'BlackIsZero' photometric interpretation (optimised for 4-bit grayscale images).
+ ///
+ internal static class BlackIsZero4TiffColor
+ {
+ ///
+ /// Decodes pixel data using the current photometric interpretation.
+ ///
+ /// The pixel format.
+ /// The buffer to read image data from.
+ /// The image buffer to write pixels to.
+ /// The x-coordinate of the left-hand side of the image block.
+ /// The y-coordinate of the top of the image block.
+ /// The width of the image block.
+ /// The height of the image block.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Decode(byte[] data, PixelAccessor pixels, int left, int top, int width, int height)
+ where TPixel : struct, IPixel
+ {
+ TPixel color = default(TPixel);
+
+ uint offset = 0;
+ bool isOddWidth = (width & 1) == 1;
+
+ for (int y = top; y < top + height; y++)
+ {
+ for (int x = left; x < left + width - 1; x += 2)
+ {
+ byte byteData = data[offset++];
+
+ byte intensity1 = (byte)(((byteData & 0xF0) >> 4) * 17);
+ color.PackFromBytes(intensity1, intensity1, intensity1, 255);
+ pixels[x, y] = color;
+
+ byte intensity2 = (byte)((byteData & 0x0F) * 17);
+ color.PackFromBytes(intensity2, intensity2, intensity2, 255);
+ pixels[x + 1, y] = color;
+ }
+
+ if (isOddWidth)
+ {
+ byte byteData = data[offset++];
+
+ byte intensity1 = (byte)(((byteData & 0xF0) >> 4) * 17);
+ color.PackFromBytes(intensity1, intensity1, intensity1, 255);
+ pixels[left + width - 1, y] = color;
+ }
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor.cs
new file mode 100644
index 0000000000..ae9cf4615f
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor.cs
@@ -0,0 +1,46 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageSharp.Formats.Tiff
+{
+ using System.Runtime.CompilerServices;
+ using ImageSharp;
+ using ImageSharp.PixelFormats;
+
+ ///
+ /// Implements the 'BlackIsZero' photometric interpretation (optimised for 8-bit grayscale images).
+ ///
+ internal static class BlackIsZero8TiffColor
+ {
+ ///
+ /// Decodes pixel data using the current photometric interpretation.
+ ///
+ /// The pixel format.
+ /// The buffer to read image data from.
+ /// The image buffer to write pixels to.
+ /// The x-coordinate of the left-hand side of the image block.
+ /// The y-coordinate of the top of the image block.
+ /// The width of the image block.
+ /// The height of the image block.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Decode(byte[] data, PixelAccessor pixels, int left, int top, int width, int height)
+ where TPixel : struct, IPixel
+ {
+ TPixel color = default(TPixel);
+
+ uint offset = 0;
+
+ for (int y = top; y < top + height; y++)
+ {
+ for (int x = left; x < left + width; x++)
+ {
+ byte intensity = data[offset++];
+ color.PackFromBytes(intensity, intensity, intensity, 255);
+ pixels[x, y] = color;
+ }
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor.cs
new file mode 100644
index 0000000000..18654f2710
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor.cs
@@ -0,0 +1,53 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageSharp.Formats.Tiff
+{
+ using System;
+ using System.Numerics;
+ using System.Runtime.CompilerServices;
+ using ImageSharp;
+ using ImageSharp.PixelFormats;
+
+ ///
+ /// Implements the 'BlackIsZero' photometric interpretation (for all bit depths).
+ ///
+ internal static class BlackIsZeroTiffColor
+ {
+ ///
+ /// Decodes pixel data using the current photometric interpretation.
+ ///
+ /// The pixel format.
+ /// The buffer to read image data from.
+ /// The number of bits per sample for each pixel.
+ /// The image buffer to write pixels to.
+ /// The x-coordinate of the left-hand side of the image block.
+ /// The y-coordinate of the top of the image block.
+ /// The width of the image block.
+ /// The height of the image block.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Decode(byte[] data, uint[] bitsPerSample, PixelAccessor pixels, int left, int top, int width, int height)
+ where TPixel : struct, IPixel
+ {
+ TPixel color = default(TPixel);
+
+ BitReader bitReader = new BitReader(data);
+ float factor = (float)Math.Pow(2, bitsPerSample[0]) - 1.0f;
+
+ for (int y = top; y < top + height; y++)
+ {
+ for (int x = left; x < left + width; x++)
+ {
+ int value = bitReader.ReadBits(bitsPerSample[0]);
+ float intensity = ((float)value) / factor;
+ color.PackFromVector4(new Vector4(intensity, intensity, intensity, 1.0f));
+ pixels[x, y] = color;
+ }
+
+ bitReader.NextRow();
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs
index b86e179595..f4a15aec26 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs
@@ -10,6 +10,26 @@ namespace ImageSharp.Formats.Tiff
///
internal enum TiffColorType
{
+ ///
+ /// Grayscale: 0 is imaged as black. The maximum value is imaged as white.
+ ///
+ BlackIsZero,
+
+ ///
+ /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Optimised implementation for bilevel images.
+ ///
+ BlackIsZero1,
+
+ ///
+ /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Optimised implementation for 4-bit images.
+ ///
+ BlackIsZero4,
+
+ ///
+ /// Grayscale: 0 is imaged as black. The maximum value is imaged as white. Optimised implementation for 8-bit images.
+ ///
+ BlackIsZero8,
+
///
/// Grayscale: 0 is imaged as white. The maximum value is imaged as black.
///
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
index 98635eca7e..9a25fa9b99 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
@@ -334,6 +334,51 @@ namespace ImageSharp.Formats
break;
}
+ case TiffPhotometricInterpretation.BlackIsZero:
+ {
+ if (ifd.TryGetIfdEntry(TiffTags.BitsPerSample, out TiffIfdEntry bitsPerSampleEntry))
+ {
+ this.BitsPerSample = this.ReadUnsignedIntegerArray(ref bitsPerSampleEntry);
+
+ if (this.BitsPerSample.Length == 1)
+ {
+ switch (this.BitsPerSample[0])
+ {
+ case 8:
+ {
+ this.ColorType = TiffColorType.BlackIsZero8;
+ break;
+ }
+
+ case 4:
+ {
+ this.ColorType = TiffColorType.BlackIsZero4;
+ break;
+ }
+
+ case 1:
+ {
+ this.ColorType = TiffColorType.BlackIsZero1;
+ break;
+ }
+
+ default:
+ {
+ this.ColorType = TiffColorType.BlackIsZero;
+ break;
+ }
+ }
+ }
+ }
+ else
+ {
+ this.ColorType = TiffColorType.BlackIsZero1;
+ this.BitsPerSample = new[] { 1u };
+ }
+
+ break;
+ }
+
default:
throw new NotSupportedException("The specified TIFF photometric interpretation is not supported.");
}
@@ -407,6 +452,18 @@ namespace ImageSharp.Formats
case TiffColorType.WhiteIsZero8:
WhiteIsZero8TiffColor.Decode(data, pixels, left, top, width, height);
break;
+ case TiffColorType.BlackIsZero:
+ BlackIsZeroTiffColor.Decode(data, this.BitsPerSample, pixels, left, top, width, height);
+ break;
+ case TiffColorType.BlackIsZero1:
+ BlackIsZero1TiffColor.Decode(data, pixels, left, top, width, height);
+ break;
+ case TiffColorType.BlackIsZero4:
+ BlackIsZero4TiffColor.Decode(data, pixels, left, top, width, height);
+ break;
+ case TiffColorType.BlackIsZero8:
+ BlackIsZero8TiffColor.Decode(data, pixels, left, top, width, height);
+ break;
default:
throw new InvalidOperationException();
}
diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColorTests.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColorTests.cs
new file mode 100644
index 0000000000..8c4f788462
--- /dev/null
+++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColorTests.cs
@@ -0,0 +1,164 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageSharp.Tests
+{
+ using System.Collections.Generic;
+ using Xunit;
+
+ using ImageSharp.Formats.Tiff;
+
+ public class BlackIsZeroTiffColorTests : PhotometricInterpretationTestBase
+ {
+ private static Rgba32 Gray000 = new Rgba32(0, 0, 0, 255);
+ private static Rgba32 Gray128 = new Rgba32(128, 128, 128, 255);
+ private static Rgba32 Gray255 = new Rgba32(255, 255, 255, 255);
+ private static Rgba32 Gray0 = new Rgba32(0, 0, 0, 255);
+ private static Rgba32 Gray8 = new Rgba32(136, 136, 136, 255);
+ private static Rgba32 GrayF = new Rgba32(255, 255, 255, 255);
+ private static Rgba32 Bit0 = new Rgba32(0, 0, 0, 255);
+ private static Rgba32 Bit1 = new Rgba32(255, 255, 255, 255);
+
+ private static byte[] Bilevel_Bytes4x4 = new byte[] { 0b01010000,
+ 0b11110000,
+ 0b01110000,
+ 0b10010000 };
+
+ private static Rgba32[][] Bilevel_Result4x4 = new[] { new[] { Bit0, Bit1, Bit0, Bit1 },
+ new[] { Bit1, Bit1, Bit1, Bit1 },
+ new[] { Bit0, Bit1, Bit1, Bit1 },
+ new[] { Bit1, Bit0, Bit0, Bit1 }};
+
+ private static byte[] Bilevel_Bytes12x4 = new byte[] { 0b01010101, 0b01010000,
+ 0b11111111, 0b11111111,
+ 0b01101001, 0b10100000,
+ 0b10010000, 0b01100000};
+
+ private static Rgba32[][] Bilevel_Result12x4 = new[] { new[] { Bit0, Bit1, Bit0, Bit1, Bit0, Bit1, Bit0, Bit1, Bit0, Bit1, Bit0, Bit1 },
+ new[] { Bit1, Bit1, Bit1, Bit1, Bit1, Bit1, Bit1, Bit1, Bit1, Bit1, Bit1, Bit1 },
+ new[] { Bit0, Bit1, Bit1, Bit0, Bit1, Bit0, Bit0, Bit1, Bit1, Bit0, Bit1, Bit0 },
+ new[] { Bit1, Bit0, Bit0, Bit1, Bit0, Bit0, Bit0, Bit0, Bit0, Bit1, Bit1, Bit0 }};
+
+ private static byte[] Grayscale4_Bytes4x4 = new byte[] { 0x8F, 0x0F,
+ 0xFF, 0xFF,
+ 0x08, 0x8F,
+ 0xF0, 0xF8 };
+
+ private static Rgba32[][] Grayscale4_Result4x4 = new[] { new[] { Gray8, GrayF, Gray0, GrayF },
+ new[] { GrayF, GrayF, GrayF, GrayF },
+ new[] { Gray0, Gray8, Gray8, GrayF },
+ new[] { GrayF, Gray0, GrayF, Gray8 }};
+
+ private static byte[] Grayscale4_Bytes3x4 = new byte[] { 0x8F, 0x00,
+ 0xFF, 0xF0,
+ 0x08, 0x80,
+ 0xF0, 0xF0 };
+
+ private static Rgba32[][] Grayscale4_Result3x4 = new[] { new[] { Gray8, GrayF, Gray0 },
+ new[] { GrayF, GrayF, GrayF },
+ new[] { Gray0, Gray8, Gray8 },
+ new[] { GrayF, Gray0, GrayF }};
+
+ private static byte[] Grayscale8_Bytes4x4 = new byte[] { 128, 255, 000, 255,
+ 255, 255, 255, 255,
+ 000, 128, 128, 255,
+ 255, 000, 255, 128 };
+
+ private static Rgba32[][] Grayscale8_Result4x4 = new[] { new[] { Gray128, Gray255, Gray000, Gray255 },
+ new[] { Gray255, Gray255, Gray255, Gray255 },
+ new[] { Gray000, Gray128, Gray128, Gray255 },
+ new[] { Gray255, Gray000, Gray255, Gray128 }};
+
+ public static IEnumerable