diff --git a/src/ImageSharp/Formats/Tiff/Compression/NoneTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/NoneTiffCompression.cs
new file mode 100644
index 000000000..c538cf473
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/Compression/NoneTiffCompression.cs
@@ -0,0 +1,28 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageSharp.Formats
+{
+ using System.IO;
+ using System.Runtime.CompilerServices;
+
+ ///
+ /// Class to handle cases where TIFF image data is not compressed.
+ ///
+ internal static class NoneTiffCompression
+ {
+ ///
+ /// Decompresses image data into the supplied buffer.
+ ///
+ /// The to read image data from.
+ /// The number of bytes to read from the input stream.
+ /// The output buffer for uncompressed data.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Decompress(Stream stream, int byteCount, byte[] buffer)
+ {
+ stream.ReadFull(buffer, byteCount);
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs
new file mode 100644
index 000000000..5b6368bf9
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressionType.cs
@@ -0,0 +1,18 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageSharp.Formats
+{
+ ///
+ /// Provides enumeration of the various TIFF compression types.
+ ///
+ internal enum TiffCompressionType
+ {
+ ///
+ /// Image data is stored uncompressed in the TIFF file.
+ ///
+ None = 0
+ }
+}
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs
new file mode 100644
index 000000000..bca27e4b2
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs
@@ -0,0 +1,18 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageSharp.Formats
+{
+ ///
+ /// Provides enumeration of the various TIFF photometric interpretation implementation types.
+ ///
+ internal enum TiffColorType
+ {
+ ///
+ /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimised implementation for 8-bit images.
+ ///
+ WhiteIsZero8
+ }
+}
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor.cs
new file mode 100644
index 000000000..295db5e18
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor.cs
@@ -0,0 +1,45 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageSharp.Formats
+{
+ using System.Runtime.CompilerServices;
+ using ImageSharp;
+
+ ///
+ /// Implements the 'WhiteIsZero' photometric interpretation (optimised for 8-bit grayscale images).
+ ///
+ internal static class WhiteIsZero8TiffColor
+ {
+ ///
+ /// 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 TColor : struct, IPixel
+ {
+ TColor color = default(TColor);
+
+ uint offset = 0;
+
+ for (int y = top; y < top + height; y++)
+ {
+ for (int x = left; x < left + width; x++)
+ {
+ byte intensity = (byte)(255 - data[offset++]);
+ color.PackFromBytes(intensity, intensity, intensity, 255);
+ pixels[x, y] = color;
+ }
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
index 28d45a6e1..f186e33ee 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
@@ -6,6 +6,7 @@
namespace ImageSharp.Formats
{
using System;
+ using System.Buffers;
using System.IO;
using System.Text;
@@ -41,6 +42,16 @@ namespace ImageSharp.Formats
this.IsLittleEndian = isLittleEndian;
}
+ ///
+ /// Gets or sets the photometric interpretation implementation to use when decoding the image.
+ ///
+ public TiffColorType ColorType { get; set; }
+
+ ///
+ /// Gets or sets the compression implementation to use when decoding the image.
+ ///
+ public TiffCompressionType CompressionType { get; set; }
+
///
/// Gets the input stream.
///
@@ -93,7 +104,7 @@ namespace ImageSharp.Formats
public uint ReadHeader()
{
byte[] headerBytes = new byte[TiffConstants.SizeOfTiffHeader];
- this.ReadBytes(headerBytes, TiffConstants.SizeOfTiffHeader);
+ this.InputStream.ReadFull(headerBytes, TiffConstants.SizeOfTiffHeader);
if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian)
{
@@ -129,13 +140,13 @@ namespace ImageSharp.Formats
byte[] buffer = new byte[TiffConstants.SizeOfIfdEntry];
- this.ReadBytes(buffer, 2);
+ this.InputStream.ReadFull(buffer, 2);
ushort entryCount = this.ToUInt16(buffer, 0);
TiffIfdEntry[] entries = new TiffIfdEntry[entryCount];
for (int i = 0; i < entryCount; i++)
{
- this.ReadBytes(buffer, TiffConstants.SizeOfIfdEntry);
+ this.InputStream.ReadFull(buffer, TiffConstants.SizeOfIfdEntry);
ushort tag = this.ToUInt16(buffer, 0);
TiffType type = (TiffType)this.ToUInt16(buffer, 2);
@@ -145,7 +156,7 @@ namespace ImageSharp.Formats
entries[i] = new TiffIfdEntry(tag, type, count, value);
}
- this.ReadBytes(buffer, 4);
+ this.InputStream.ReadFull(buffer, 4);
uint nextIfdOffset = this.ToUInt32(buffer, 0);
return new TiffIfd(entries, nextIfdOffset);
@@ -177,18 +188,184 @@ namespace ImageSharp.Formats
resolutionUnit = (TiffResolutionUnit)this.ReadUnsignedInteger(ref resolutionUnitEntry);
}
- double resolutionUnitFactor = resolutionUnit == TiffResolutionUnit.Centimeter ? 1.0 / 2.54 : 1.0;
+ if (resolutionUnit != TiffResolutionUnit.None)
+ {
+ double resolutionUnitFactor = resolutionUnit == TiffResolutionUnit.Centimeter ? 2.54 : 1.0;
+
+ if (ifd.TryGetIfdEntry(TiffTags.XResolution, out TiffIfdEntry xResolutionEntry))
+ {
+ Rational xResolution = this.ReadUnsignedRational(ref xResolutionEntry);
+ image.MetaData.HorizontalResolution = xResolution.ToDouble() * resolutionUnitFactor;
+ }
+
+ if (ifd.TryGetIfdEntry(TiffTags.YResolution, out TiffIfdEntry yResolutionEntry))
+ {
+ Rational yResolution = this.ReadUnsignedRational(ref yResolutionEntry);
+ image.MetaData.VerticalResolution = yResolution.ToDouble() * resolutionUnitFactor;
+ }
+ }
+
+ this.ReadImageFormat(ifd);
+
+ if (ifd.TryGetIfdEntry(TiffTags.RowsPerStrip, out TiffIfdEntry rowsPerStripEntry)
+ && ifd.TryGetIfdEntry(TiffTags.StripOffsets, out TiffIfdEntry stripOffsetsEntry)
+ && ifd.TryGetIfdEntry(TiffTags.StripByteCounts, out TiffIfdEntry stripByteCountsEntry))
+ {
+ int rowsPerStrip = (int)this.ReadUnsignedInteger(ref rowsPerStripEntry);
+ uint[] stripOffsets = this.ReadUnsignedIntegerArray(ref stripOffsetsEntry);
+ uint[] stripByteCounts = this.ReadUnsignedIntegerArray(ref stripByteCountsEntry);
+
+ int uncompressedStripSize = this.CalculateImageBufferSize(width, rowsPerStrip);
+
+ using (PixelAccessor pixels = image.Lock())
+ {
+ byte[] stripBytes = ArrayPool.Shared.Rent(uncompressedStripSize);
+
+ try
+ {
+ this.DecompressImageBlock(stripOffsets[0], stripByteCounts[0], stripBytes);
+ this.ProcessImageBlock(stripBytes, pixels, 0, 0, width, rowsPerStrip);
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(stripBytes);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Determines the TIFF compression and color types, and reads any associated parameters.
+ ///
+ /// The IFD to read the image format information for.
+ public void ReadImageFormat(TiffIfd ifd)
+ {
+ TiffCompression compression = TiffCompression.None;
- if (ifd.TryGetIfdEntry(TiffTags.XResolution, out TiffIfdEntry xResolutionEntry))
+ if (ifd.TryGetIfdEntry(TiffTags.Compression, out TiffIfdEntry compressionEntry))
{
- Rational xResolution = this.ReadUnsignedRational(ref xResolutionEntry);
- image.MetaData.HorizontalResolution = xResolution.ToDouble();
+ compression = (TiffCompression)this.ReadUnsignedInteger(ref compressionEntry);
}
-
- if (ifd.TryGetIfdEntry(TiffTags.YResolution, out TiffIfdEntry yResolutionEntry))
+
+ switch (compression)
{
- Rational yResolution = this.ReadUnsignedRational(ref yResolutionEntry);
- image.MetaData.VerticalResolution = yResolution.ToDouble();
+ case TiffCompression.None:
+ {
+ this.CompressionType = TiffCompressionType.None;
+ break;
+ }
+
+ default:
+ {
+ throw new NotSupportedException("The specified TIFF compression format is not supported.");
+ }
+ }
+
+ TiffPhotometricInterpretation photometricInterpretation;
+
+ if (ifd.TryGetIfdEntry(TiffTags.PhotometricInterpretation, out TiffIfdEntry photometricInterpretationEntry))
+ {
+ photometricInterpretation = (TiffPhotometricInterpretation)this.ReadUnsignedInteger(ref photometricInterpretationEntry);
+ }
+ else
+ {
+ if (compression == TiffCompression.Ccitt1D)
+ {
+ photometricInterpretation = TiffPhotometricInterpretation.WhiteIsZero;
+ }
+ else
+ {
+ throw new ImageFormatException("The TIFF photometric interpretation entry is missing.");
+ }
+ }
+
+ switch (photometricInterpretation)
+ {
+ case TiffPhotometricInterpretation.WhiteIsZero:
+ {
+ if (ifd.TryGetIfdEntry(TiffTags.BitsPerSample, out TiffIfdEntry bitsPerSampleEntry))
+ {
+ uint[] bitsPerSample = this.ReadUnsignedIntegerArray(ref bitsPerSampleEntry);
+
+ if (bitsPerSample.Length == 1 && bitsPerSample[0] == 8)
+ {
+ this.ColorType = TiffColorType.WhiteIsZero8;
+ }
+ else
+ {
+ throw new NotSupportedException("The specified TIFF bit-depth is not supported.");
+ }
+ }
+ else
+ {
+ throw new NotSupportedException("TIFF bilevel images are not supported.");
+ }
+
+ break;
+ }
+
+ default:
+ throw new NotSupportedException("The specified TIFF photometric interpretation is not supported.");
+ }
+ }
+
+ ///
+ /// Calculates the size (in bytes) for a pixel buffer using the determined color format.
+ ///
+ /// The width for the desired pixel buffer.
+ /// The height for the desired pixel buffer.
+ /// The size (in bytes) of the required pixel buffer.
+ public int CalculateImageBufferSize(int width, int height)
+ {
+ switch (this.ColorType)
+ {
+ case TiffColorType.WhiteIsZero8:
+ return width * height;
+ default:
+ throw new InvalidOperationException();
+ }
+ }
+
+ ///
+ /// Decompresses an image block from the input stream into the specified buffer.
+ ///
+ /// The offset within the file of the image block.
+ /// The size (in bytes) of the compressed data.
+ /// The buffer to write the uncompressed data.
+ public void DecompressImageBlock(uint offset, uint byteCount, byte[] buffer)
+ {
+ this.InputStream.Seek(offset, SeekOrigin.Begin);
+
+ switch (this.CompressionType)
+ {
+ case TiffCompressionType.None:
+ NoneTiffCompression.Decompress(this.InputStream, (int)byteCount, buffer);
+ break;
+ default:
+ throw new InvalidOperationException();
+ }
+ }
+
+ ///
+ /// 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.
+ public void ProcessImageBlock(byte[] data, PixelAccessor pixels, int left, int top, int width, int height)
+ where TColor : struct, IPixel
+ {
+ switch (this.ColorType)
+ {
+ case TiffColorType.WhiteIsZero8:
+ WhiteIsZero8TiffColor.Decode(data, pixels, left, top, width, height);
+ break;
+ default:
+ throw new InvalidOperationException();
}
}
@@ -207,7 +384,7 @@ namespace ImageSharp.Formats
this.InputStream.Seek(offset, SeekOrigin.Begin);
byte[] data = new byte[byteLength];
- this.ReadBytes(data, (int)byteLength);
+ this.InputStream.ReadFull(data, (int)byteLength);
entry.Value = data;
}
@@ -621,29 +798,6 @@ namespace ImageSharp.Formats
}
}
- ///
- /// Reads a sequence of bytes from the input stream into a buffer.
- ///
- /// A buffer to store the retrieved data.
- /// The number of bytes to read.
- private void ReadBytes(byte[] buffer, int count)
- {
- int offset = 0;
-
- while (count > 0)
- {
- int bytesRead = this.InputStream.Read(buffer, offset, count);
-
- if (bytesRead == 0)
- {
- break;
- }
-
- offset += bytesRead;
- count -= bytesRead;
- }
- }
-
///
/// Converts buffer data into an using the correct endianness.
///
diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs
new file mode 100644
index 000000000..e4049cf0f
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs
@@ -0,0 +1,38 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+namespace ImageSharp.Formats
+{
+ using System.IO;
+
+ ///
+ /// TIFF specific utilities and extension methods.
+ ///
+ internal static class TiffUtils
+ {
+ ///
+ /// Reads a sequence of bytes from the input stream into a buffer.
+ ///
+ /// The stream to read from.
+ /// A buffer to store the retrieved data.
+ /// The number of bytes to read.
+ public static void ReadFull(this Stream stream, byte[] buffer, int count)
+ {
+ int offset = 0;
+
+ while (count > 0)
+ {
+ int bytesRead = stream.Read(buffer, offset, count);
+
+ if (bytesRead == 0)
+ {
+ break;
+ }
+
+ offset += bytesRead;
+ count -= bytesRead;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs
new file mode 100644
index 000000000..e3277eb96
--- /dev/null
+++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs
@@ -0,0 +1,28 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageSharp.Tests
+{
+ using System.IO;
+ using Xunit;
+
+ using ImageSharp.Formats;
+
+ public class NoneTiffCompressionTests
+ {
+ [Theory]
+ [InlineData(new byte[] { 10, 15, 20, 25, 30, 35, 40, 45 }, 8, new byte[] { 10, 15, 20, 25, 30, 35, 40, 45 })]
+ [InlineData(new byte[] { 10, 15, 20, 25, 30, 35, 40, 45 }, 5, new byte[] { 10, 15, 20, 25, 30 })]
+ public void Decompress_ReadsData(byte[] inputData, int byteCount, byte[] expectedResult)
+ {
+ Stream stream = new MemoryStream(inputData);
+ byte[] buffer = new byte[expectedResult.Length];
+
+ NoneTiffCompression.Decompress(stream, byteCount, buffer);
+
+ Assert.Equal(expectedResult, buffer);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/PhotometricInterpretationTestBase.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/PhotometricInterpretationTestBase.cs
new file mode 100644
index 000000000..7fdb12177
--- /dev/null
+++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/PhotometricInterpretationTestBase.cs
@@ -0,0 +1,59 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageSharp.Tests
+{
+ using System;
+ using Xunit;
+
+ public abstract class PhotometricInterpretationTestBase
+ {
+ public static Color[][] Offset(Color[][] input, int xOffset, int yOffset, int width, int height)
+ {
+ int inputHeight = input.Length;
+ int inputWidth = input[0].Length;
+
+ Color[][] output = new Color[height][];
+
+ for (int y = 0; y < output.Length; y++)
+ {
+ output[y] = new Color[width];
+ }
+
+ for (int y = 0; y < inputHeight; y++)
+ {
+ for (int x = 0; x < inputWidth; x++)
+ {
+ output[y + yOffset][x + xOffset] = input[y][x];
+ }
+ }
+
+ return output;
+ }
+
+ public static void AssertDecode(Color[][] expectedResult, Action> decodeAction)
+ {
+ int resultWidth = expectedResult[0].Length;
+ int resultHeight = expectedResult.Length;
+ Image image = new Image(resultWidth, resultHeight);
+
+ using (PixelAccessor pixels = image.Lock())
+ {
+ decodeAction(pixels);
+ }
+
+ using (PixelAccessor pixels = image.Lock())
+ {
+ for (int y = 0; y < resultHeight; y++)
+ {
+ for (int x = 0; x < resultWidth; x++)
+ {
+ Assert.Equal(expectedResult[y][x], pixels[x, y]);
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColorTests.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColorTests.cs
new file mode 100644
index 000000000..075881f61
--- /dev/null
+++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColorTests.cs
@@ -0,0 +1,51 @@
+//
+// 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;
+
+ public class WhiteIsZero8TiffColorTests : PhotometricInterpretationTestBase
+ {
+ private static Color Gray000 = new Color(255, 255, 255, 255);
+ private static Color Gray128 = new Color(127, 127, 127, 255);
+ private static Color Gray255 = new Color(0, 0, 0, 255);
+
+ private static byte[] GrayscaleBytes4x4 = new byte[] { 128, 255, 000, 255,
+ 255, 255, 255, 255,
+ 000, 128, 128, 255,
+ 255, 000, 255, 128 };
+
+ private static Color[][] GrayscaleResult4x4 = new[] { new[] { Gray128, Gray255, Gray000, Gray255 },
+ new[] { Gray255, Gray255, Gray255, Gray255 },
+ new[] { Gray000, Gray128, Gray128, Gray255 },
+ new[] { Gray255, Gray000, Gray255, Gray128 }};
+
+ public static IEnumerable