From 5e4c4f392d001aea06faf5a1fec17e553e242b5e Mon Sep 17 00:00:00 2001 From: Andrew Wilkinson Date: Mon, 15 May 2017 15:49:58 +0100 Subject: [PATCH] Add default WhiteIsZero implementation --- .../TiffColorType.cs | 5 ++ .../WhiteIsZeroTiffColor.cs | 53 +++++++++++++++ .../Formats/Tiff/TiffDecoderCore.cs | 33 ++++++---- .../Formats/Tiff/Utils/BitReader.cs | 65 +++++++++++++++++++ .../WhiteIsZeroTiffColorTests.cs | 12 ++++ .../Formats/Tiff/TiffDecoderImageTests.cs | 52 +++++++++++---- 6 files changed, 194 insertions(+), 26 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor.cs create mode 100644 src/ImageSharp/Formats/Tiff/Utils/BitReader.cs diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs index be9e14df40..b86e179595 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -10,6 +10,11 @@ namespace ImageSharp.Formats.Tiff /// internal enum TiffColorType { + /// + /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. + /// + WhiteIsZero, + /// /// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimised implementation for bilevel images. /// diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor.cs new file mode 100644 index 0000000000..876ea87890 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor.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 'WhiteIsZero' photometric interpretation (for all bit depths). + /// + internal static class WhiteIsZeroTiffColor + { + /// + /// 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 = 1.0f - (((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/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 225bddb1ed..98635eca7e 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -52,6 +52,11 @@ namespace ImageSharp.Formats this.IsLittleEndian = isLittleEndian; } + /// + /// Gets or sets the number of bits for each sample of the pixel format used to encode the image. + /// + public uint[] BitsPerSample { get; set; } + /// /// Gets or sets the photometric interpretation implementation to use when decoding the image. /// @@ -288,11 +293,11 @@ namespace ImageSharp.Formats { if (ifd.TryGetIfdEntry(TiffTags.BitsPerSample, out TiffIfdEntry bitsPerSampleEntry)) { - uint[] bitsPerSample = this.ReadUnsignedIntegerArray(ref bitsPerSampleEntry); + this.BitsPerSample = this.ReadUnsignedIntegerArray(ref bitsPerSampleEntry); - if (bitsPerSample.Length == 1) + if (this.BitsPerSample.Length == 1) { - switch (bitsPerSample[0]) + switch (this.BitsPerSample[0]) { case 8: { @@ -314,7 +319,8 @@ namespace ImageSharp.Formats default: { - throw new NotSupportedException("The specified TIFF bit-depth is not supported."); + this.ColorType = TiffColorType.WhiteIsZero; + break; } } } @@ -322,6 +328,7 @@ namespace ImageSharp.Formats else { this.ColorType = TiffColorType.WhiteIsZero1; + this.BitsPerSample = new[] { 1u }; } break; @@ -340,17 +347,14 @@ namespace ImageSharp.Formats /// The size (in bytes) of the required pixel buffer. public int CalculateImageBufferSize(int width, int height) { - switch (this.ColorType) + uint bitsPerPixel = 0; + for (int i = 0; i < this.BitsPerSample.Length; i++) { - case TiffColorType.WhiteIsZero1: - return ((width + 7) / 8) * height; - case TiffColorType.WhiteIsZero4: - return ((width + 1) / 2) * height; - case TiffColorType.WhiteIsZero8: - return width * height; - default: - throw new InvalidOperationException(); + bitsPerPixel += this.BitsPerSample[i]; } + + int bytesPerRow = ((width * (int)bitsPerPixel) + 7) / 8; + return bytesPerRow * height; } /// @@ -391,6 +395,9 @@ namespace ImageSharp.Formats { switch (this.ColorType) { + case TiffColorType.WhiteIsZero: + WhiteIsZeroTiffColor.Decode(data, this.BitsPerSample, pixels, left, top, width, height); + break; case TiffColorType.WhiteIsZero1: WhiteIsZero1TiffColor.Decode(data, pixels, left, top, width, height); break; diff --git a/src/ImageSharp/Formats/Tiff/Utils/BitReader.cs b/src/ImageSharp/Formats/Tiff/Utils/BitReader.cs new file mode 100644 index 0000000000..f330690f9f --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Utils/BitReader.cs @@ -0,0 +1,65 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// +namespace ImageSharp.Formats.Tiff +{ + using System.IO; + + /// + /// Utility class to read a sequence of bits from an array + /// + internal class BitReader + { + private readonly byte[] array; + private int offset; + private int bitOffset; + + /// + /// Initializes a new instance of the class. + /// + /// The array to read data from. + public BitReader(byte[] array) + { + this.array = array; + } + + /// + /// Reads the specified number of bits from the array. + /// + /// The number of bits to read. + /// The value read from the array. + public int ReadBits(uint bits) + { + int value = 0; + + for (uint i = 0; i < bits; i++) + { + int bit = (this.array[this.offset] >> (7 - this.bitOffset)) & 0x01; + value = (value << 1) | bit; + + this.bitOffset++; + + if (this.bitOffset == 8) + { + this.bitOffset = 0; + this.offset++; + } + } + + return value; + } + + /// + /// Moves the reader to the next row of byte-aligned data. + /// + public void NextRow() + { + if (this.bitOffset > 0) + { + this.bitOffset = 0; + this.offset++; + } + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColorTests.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColorTests.cs index ce04e0225a..e9d9556bd7 100644 --- a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColorTests.cs +++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColorTests.cs @@ -119,6 +119,18 @@ namespace ImageSharp.Tests } } + [Theory] + [MemberData(nameof(Bilevel_Data))] + [MemberData(nameof(Grayscale4_Data))] + [MemberData(nameof(Grayscale8_Data))] + public void Decode_WritesPixelData(byte[] inputData, int bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult) + { + AssertDecode(expectedResult, pixels => + { + WhiteIsZeroTiffColor.Decode(inputData, new[] { (uint)bitsPerSample }, pixels, left, top, width, height); + }); + } + [Theory] [MemberData(nameof(Bilevel_Data))] public void Decode_WritesPixelData_Bilevel(byte[] inputData, int bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult) diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs index 2cf6384597..168ecf0bc2 100644 --- a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs +++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs @@ -176,6 +176,8 @@ namespace ImageSharp.Tests } [Theory] + [InlineData(false, TiffPhotometricInterpretation.WhiteIsZero, new[] { 3 }, TiffColorType.WhiteIsZero)] + [InlineData(true, TiffPhotometricInterpretation.WhiteIsZero, new[] { 3 }, TiffColorType.WhiteIsZero)] [InlineData(false, TiffPhotometricInterpretation.WhiteIsZero, new[] { 8 }, TiffColorType.WhiteIsZero8)] [InlineData(true, TiffPhotometricInterpretation.WhiteIsZero, new[] { 8 }, TiffColorType.WhiteIsZero8)] [InlineData(false, TiffPhotometricInterpretation.WhiteIsZero, new[] { 4 }, TiffColorType.WhiteIsZero4)] @@ -287,33 +289,57 @@ namespace ImageSharp.Tests } [Theory] - [InlineData(false, TiffPhotometricInterpretation.WhiteIsZero, new[] { 3 })] - [InlineData(true, TiffPhotometricInterpretation.WhiteIsZero, new[] { 3 })] - public void ReadImageFormat_ThrowsExceptionForUnsupportedBitDepth(bool isLittleEndian, ushort photometricInterpretation, int[] bitsPerSample) + [InlineData(false, new[] { 8u })] + [InlineData(true, new[] { 8u })] + [InlineData(false, new[] { 4u })] + [InlineData(true, new[] { 4u })] + [InlineData(false, new[] { 1u })] + [InlineData(true, new[] { 1u })] + [InlineData(false, new[] { 1u, 2u, 3u })] + [InlineData(true, new[] { 1u, 2u, 3u })] + [InlineData(false, new[] { 8u, 8u, 8u })] + [InlineData(true, new[] { 8u, 8u, 8u })] + public void ReadImageFormat_ReadsBitsPerSample(bool isLittleEndian, uint[] bitsPerSample) { Stream stream = CreateTiffGenIfd() - .WithEntry(TiffGenEntry.Integer(TiffTags.PhotometricInterpretation, TiffType.Short, photometricInterpretation)) .WithEntry(TiffGenEntry.Integer(TiffTags.BitsPerSample, TiffType.Short, bitsPerSample)) .ToStream(isLittleEndian); TiffDecoderCore decoder = new TiffDecoderCore(stream, isLittleEndian, null, null); TiffIfd ifd = decoder.ReadIfd(0); + decoder.ReadImageFormat(ifd); - var e = Assert.Throws(() => decoder.ReadImageFormat(ifd)); + Assert.Equal(bitsPerSample, decoder.BitsPerSample); + } + + [Theory] + [InlineData(false, TiffPhotometricInterpretation.WhiteIsZero)] + [InlineData(true, TiffPhotometricInterpretation.WhiteIsZero)] + public void ReadImageFormat_ReadsBitsPerSample_DefaultsToBilevel(bool isLittleEndian, ushort photometricInterpretation) + { + Stream stream = CreateTiffGenIfd() + .WithEntry(TiffGenEntry.Integer(TiffTags.PhotometricInterpretation, TiffType.Short, photometricInterpretation)) + .WithoutEntry(TiffTags.BitsPerSample) + .ToStream(isLittleEndian); + + TiffDecoderCore decoder = new TiffDecoderCore(stream, isLittleEndian, null, null); + TiffIfd ifd = decoder.ReadIfd(0); + decoder.ReadImageFormat(ifd); - Assert.Equal("The specified TIFF bit-depth is not supported.", e.Message); + Assert.Equal(new[] { 1u }, decoder.BitsPerSample); } [Theory] - [InlineData(TiffColorType.WhiteIsZero8, 100, 80, 100 * 80)] - [InlineData(TiffColorType.WhiteIsZero4, 100, 80, 50 * 80)] - [InlineData(TiffColorType.WhiteIsZero4, 99, 80, 50 * 80)] - [InlineData(TiffColorType.WhiteIsZero1, 160, 80, 20 * 80)] - [InlineData(TiffColorType.WhiteIsZero1, 153, 80, 20 * 80)] - public void CalculateImageBufferSize_ReturnsCorrectSize(ushort colorType, int width, int height, int expectedResult) + [InlineData(new uint[] { 1 }, 160, 80, 20 * 80)] + [InlineData(new uint[] { 1 }, 153, 80, 20 * 80)] + [InlineData(new uint[] { 3 }, 100, 80, 38 * 80)] + [InlineData(new uint[] { 4 }, 100, 80, 50 * 80)] + [InlineData(new uint[] { 4 }, 99, 80, 50 * 80)] + [InlineData(new uint[] { 8 }, 100, 80, 100 * 80)] + public void CalculateImageBufferSize_ReturnsCorrectSize(uint[] bitsPerSample, int width, int height, int expectedResult) { TiffDecoderCore decoder = new TiffDecoderCore(null, null); - decoder.ColorType = (TiffColorType)colorType; + decoder.BitsPerSample = bitsPerSample; int bufferSize = decoder.CalculateImageBufferSize(width, height);