Browse Source

Add support for WhiteIsZero bilevel & 4-bit images

pull/1570/head
Andrew Wilkinson 9 years ago
parent
commit
05f0937652
  1. 10
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs
  2. 53
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero1TiffColor.cs
  3. 61
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero4TiffColor.cs
  4. 2
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor.cs
  5. 44
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  6. 3
      tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/PhotometricInterpretationTestBase.cs
  7. 51
      tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColorTests.cs
  8. 152
      tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColorTests.cs
  9. 26
      tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs

10
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs

@ -10,6 +10,16 @@ namespace ImageSharp.Formats.Tiff
/// </summary>
internal enum TiffColorType
{
/// <summary>
/// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimised implementation for bilevel images.
/// </summary>
WhiteIsZero1,
/// <summary>
/// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimised implementation for 4-bit images.
/// </summary>
WhiteIsZero4,
/// <summary>
/// Grayscale: 0 is imaged as white. The maximum value is imaged as black. Optimised implementation for 8-bit images.
/// </summary>

53
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero1TiffColor.cs

@ -0,0 +1,53 @@
// <copyright file="WhiteIsZero1TiffColor.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Formats.Tiff
{
using System;
using System.Runtime.CompilerServices;
using ImageSharp;
/// <summary>
/// Implements the 'WhiteIsZero' photometric interpretation (optimised for bilevel images).
/// </summary>
internal static class WhiteIsZero1TiffColor
{
/// <summary>
/// Decodes pixel data using the current photometric interpretation.
/// </summary>
/// <typeparam name="TColor">The pixel format.</typeparam>
/// <param name="data">The buffer to read image data from.</param>
/// <param name="pixels">The image buffer to write pixels to.</param>
/// <param name="left">The x-coordinate of the left-hand side of the image block.</param>
/// <param name="top">The y-coordinate of the top of the image block.</param>
/// <param name="width">The width of the image block.</param>
/// <param name="height">The height of the image block.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Decode<TColor>(byte[] data, PixelAccessor<TColor> pixels, int left, int top, int width, int height)
where TColor : struct, IPixel<TColor>
{
TColor color = default(TColor);
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)0 : (byte)255;
color.PackFromBytes(intensity, intensity, intensity, 255);
pixels[x + shift, y] = color;
}
}
}
}
}
}

61
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero4TiffColor.cs

@ -0,0 +1,61 @@
// <copyright file="WhiteIsZero4TiffColor.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Formats.Tiff
{
using System.Runtime.CompilerServices;
using ImageSharp;
/// <summary>
/// Implements the 'WhiteIsZero' photometric interpretation (optimised for 4-bit grayscale images).
/// </summary>
internal static class WhiteIsZero4TiffColor
{
/// <summary>
/// Decodes pixel data using the current photometric interpretation.
/// </summary>
/// <typeparam name="TColor">The pixel format.</typeparam>
/// <param name="data">The buffer to read image data from.</param>
/// <param name="pixels">The image buffer to write pixels to.</param>
/// <param name="left">The x-coordinate of the left-hand side of the image block.</param>
/// <param name="top">The y-coordinate of the top of the image block.</param>
/// <param name="width">The width of the image block.</param>
/// <param name="height">The height of the image block.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Decode<TColor>(byte[] data, PixelAccessor<TColor> pixels, int left, int top, int width, int height)
where TColor : struct, IPixel<TColor>
{
TColor color = default(TColor);
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)((15 - ((byteData & 0xF0) >> 4)) * 17);
color.PackFromBytes(intensity1, intensity1, intensity1, 255);
pixels[x, y] = color;
byte intensity2 = (byte)((15 - (byteData & 0x0F)) * 17);
color.PackFromBytes(intensity2, intensity2, intensity2, 255);
pixels[x + 1, y] = color;
}
if (isOddWidth)
{
byte byteData = data[offset++];
byte intensity1 = (byte)((15 - ((byteData & 0xF0) >> 4)) * 17);
color.PackFromBytes(intensity1, intensity1, intensity1, 255);
pixels[left + width - 1, y] = color;
}
}
}
}
}

2
src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor.cs

@ -9,7 +9,7 @@ namespace ImageSharp.Formats.Tiff
using ImageSharp;
/// <summary>
/// Implements the 'WhiteIsZero' photometric interpretation (optimised for 8-bit grayscale images).
/// Implements the 'WhiteIsZero' photometric interpretation (optimised for 8-bit grayscale images).
/// </summary>
internal static class WhiteIsZero8TiffColor
{

44
src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

@ -299,18 +299,38 @@ namespace ImageSharp.Formats
{
uint[] bitsPerSample = this.ReadUnsignedIntegerArray(ref bitsPerSampleEntry);
if (bitsPerSample.Length == 1 && bitsPerSample[0] == 8)
if (bitsPerSample.Length == 1)
{
this.ColorType = TiffColorType.WhiteIsZero8;
}
else
{
throw new NotSupportedException("The specified TIFF bit-depth is not supported.");
switch (bitsPerSample[0])
{
case 8:
{
this.ColorType = TiffColorType.WhiteIsZero8;
break;
}
case 4:
{
this.ColorType = TiffColorType.WhiteIsZero4;
break;
}
case 1:
{
this.ColorType = TiffColorType.WhiteIsZero1;
break;
}
default:
{
throw new NotSupportedException("The specified TIFF bit-depth is not supported.");
}
}
}
}
else
{
throw new NotSupportedException("TIFF bilevel images are not supported.");
this.ColorType = TiffColorType.WhiteIsZero1;
}
break;
@ -331,6 +351,10 @@ namespace ImageSharp.Formats
{
switch (this.ColorType)
{
case TiffColorType.WhiteIsZero1:
return ((width + 7) / 8) * height;
case TiffColorType.WhiteIsZero4:
return ((width + 1) / 2) * height;
case TiffColorType.WhiteIsZero8:
return width * height;
default:
@ -373,6 +397,12 @@ namespace ImageSharp.Formats
{
switch (this.ColorType)
{
case TiffColorType.WhiteIsZero1:
WhiteIsZero1TiffColor.Decode(data, pixels, left, top, width, height);
break;
case TiffColorType.WhiteIsZero4:
WhiteIsZero4TiffColor.Decode(data, pixels, left, top, width, height);
break;
case TiffColorType.WhiteIsZero8:
WhiteIsZero8TiffColor.Decode(data, pixels, left, top, width, height);
break;

3
tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/PhotometricInterpretationTestBase.cs

@ -50,7 +50,8 @@ namespace ImageSharp.Tests
{
for (int x = 0; x < resultWidth; x++)
{
Assert.Equal(expectedResult[y][x], pixels[x, y]);
Assert.True(expectedResult[y][x] == pixels[x, y],
$"Pixel ({x}, {y}) should be {expectedResult[y][x]} but was {pixels[x,y]}");
}
}
}

51
tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColorTests.cs

@ -1,51 +0,0 @@
// <copyright file="WhiteIsZero8TiffColorTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Tests
{
using System.Collections.Generic;
using Xunit;
using ImageSharp.Formats.Tiff;
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<object[]> DecodeData
{
get
{
yield return new object[] { GrayscaleBytes4x4, 0, 0, 4, 4, GrayscaleResult4x4 };
yield return new object[] { GrayscaleBytes4x4, 0, 0, 4, 4, Offset(GrayscaleResult4x4, 0, 0, 6, 6) };
yield return new object[] { GrayscaleBytes4x4, 1, 0, 4, 4, Offset(GrayscaleResult4x4, 1, 0, 6, 6) };
yield return new object[] { GrayscaleBytes4x4, 0, 1, 4, 4, Offset(GrayscaleResult4x4, 0, 1, 6, 6) };
yield return new object[] { GrayscaleBytes4x4, 1, 1, 4, 4, Offset(GrayscaleResult4x4, 1, 1, 6, 6) };
}
}
[Theory]
[MemberData(nameof(DecodeData))]
public void Decode_WritesPixelData(byte[] inputData, int left, int top, int width, int height, Color[][] expectedResult)
{
AssertDecode(expectedResult, pixels =>
{
WhiteIsZero8TiffColor.Decode(inputData, pixels, left, top, width, height);
});
}
}
}

152
tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColorTests.cs

@ -0,0 +1,152 @@
// <copyright file="WhiteIsZeroTiffColorTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Tests
{
using System.Collections.Generic;
using Xunit;
using ImageSharp.Formats.Tiff;
public class WhiteIsZeroTiffColorTests : 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 Color Gray0 = new Color(255, 255, 255, 255);
private static Color Gray8 = new Color(119, 119, 119, 255);
private static Color GrayF = new Color(0, 0, 0, 255);
private static Color Bit0 = new Color(255, 255, 255, 255);
private static Color Bit1 = new Color(0, 0, 0, 255);
private static byte[] Bilevel_Bytes4x4 = new byte[] { 0b01010000,
0b11110000,
0b01110000,
0b10010000 };
private static Color[][] 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 Color[][] 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 Color[][] 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 Color[][] 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 Color[][] 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<object[]> Bilevel_Data
{
get
{
yield return new object[] { Bilevel_Bytes4x4, 1, 0, 0, 4, 4, Bilevel_Result4x4 };
yield return new object[] { Bilevel_Bytes4x4, 1, 0, 0, 4, 4, Offset(Bilevel_Result4x4, 0, 0, 6, 6) };
yield return new object[] { Bilevel_Bytes4x4, 1, 1, 0, 4, 4, Offset(Bilevel_Result4x4, 1, 0, 6, 6) };
yield return new object[] { Bilevel_Bytes4x4, 1, 0, 1, 4, 4, Offset(Bilevel_Result4x4, 0, 1, 6, 6) };
yield return new object[] { Bilevel_Bytes4x4, 1, 1, 1, 4, 4, Offset(Bilevel_Result4x4, 1, 1, 6, 6) };
yield return new object[] { Bilevel_Bytes12x4, 1, 0, 0, 12, 4, Bilevel_Result12x4 };
yield return new object[] { Bilevel_Bytes12x4, 1, 0, 0, 12, 4, Offset(Bilevel_Result12x4, 0, 0, 18, 6) };
yield return new object[] { Bilevel_Bytes12x4, 1, 1, 0, 12, 4, Offset(Bilevel_Result12x4, 1, 0, 18, 6) };
yield return new object[] { Bilevel_Bytes12x4, 1, 0, 1, 12, 4, Offset(Bilevel_Result12x4, 0, 1, 18, 6) };
yield return new object[] { Bilevel_Bytes12x4, 1, 1, 1, 12, 4, Offset(Bilevel_Result12x4, 1, 1, 18, 6) };
}
}
public static IEnumerable<object[]> Grayscale4_Data
{
get
{
yield return new object[] { Grayscale4_Bytes4x4, 4, 0, 0, 4, 4, Grayscale4_Result4x4 };
yield return new object[] { Grayscale4_Bytes4x4, 4, 0, 0, 4, 4, Offset(Grayscale4_Result4x4, 0, 0, 6, 6) };
yield return new object[] { Grayscale4_Bytes4x4, 4, 1, 0, 4, 4, Offset(Grayscale4_Result4x4, 1, 0, 6, 6) };
yield return new object[] { Grayscale4_Bytes4x4, 4, 0, 1, 4, 4, Offset(Grayscale4_Result4x4, 0, 1, 6, 6) };
yield return new object[] { Grayscale4_Bytes4x4, 4, 1, 1, 4, 4, Offset(Grayscale4_Result4x4, 1, 1, 6, 6) };
yield return new object[] { Grayscale4_Bytes3x4, 4, 0, 0, 3, 4, Grayscale4_Result3x4 };
yield return new object[] { Grayscale4_Bytes3x4, 4, 0, 0, 3, 4, Offset(Grayscale4_Result3x4, 0, 0, 6, 6) };
yield return new object[] { Grayscale4_Bytes3x4, 4, 1, 0, 3, 4, Offset(Grayscale4_Result3x4, 1, 0, 6, 6) };
yield return new object[] { Grayscale4_Bytes3x4, 4, 0, 1, 3, 4, Offset(Grayscale4_Result3x4, 0, 1, 6, 6) };
yield return new object[] { Grayscale4_Bytes3x4, 4, 1, 1, 3, 4, Offset(Grayscale4_Result3x4, 1, 1, 6, 6) };
}
}
public static IEnumerable<object[]> Grayscale8_Data
{
get
{
yield return new object[] { Grayscale8_Bytes4x4, 8, 0, 0, 4, 4, Grayscale8_Result4x4 };
yield return new object[] { Grayscale8_Bytes4x4, 8, 0, 0, 4, 4, Offset(Grayscale8_Result4x4, 0, 0, 6, 6) };
yield return new object[] { Grayscale8_Bytes4x4, 8, 1, 0, 4, 4, Offset(Grayscale8_Result4x4, 1, 0, 6, 6) };
yield return new object[] { Grayscale8_Bytes4x4, 8, 0, 1, 4, 4, Offset(Grayscale8_Result4x4, 0, 1, 6, 6) };
yield return new object[] { Grayscale8_Bytes4x4, 8, 1, 1, 4, 4, Offset(Grayscale8_Result4x4, 1, 1, 6, 6) };
}
}
[Theory]
[MemberData(nameof(Bilevel_Data))]
public void Decode_WritesPixelData_Bilevel(byte[] inputData, int bitsPerSample, int left, int top, int width, int height, Color[][] expectedResult)
{
AssertDecode(expectedResult, pixels =>
{
WhiteIsZero1TiffColor.Decode(inputData, pixels, left, top, width, height);
});
}
[Theory]
[MemberData(nameof(Grayscale4_Data))]
public void Decode_WritesPixelData_4Bit(byte[] inputData, int bitsPerSample, int left, int top, int width, int height, Color[][] expectedResult)
{
AssertDecode(expectedResult, pixels =>
{
WhiteIsZero4TiffColor.Decode(inputData, pixels, left, top, width, height);
});
}
[Theory]
[MemberData(nameof(Grayscale8_Data))]
public void Decode_WritesPixelData_8Bit(byte[] inputData, int bitsPerSample, int left, int top, int width, int height, Color[][] expectedResult)
{
AssertDecode(expectedResult, pixels =>
{
WhiteIsZero8TiffColor.Decode(inputData, pixels, left, top, width, height);
});
}
}
}

26
tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs

@ -178,6 +178,10 @@ namespace ImageSharp.Tests
[Theory]
[InlineData(false, TiffPhotometricInterpretation.WhiteIsZero, new[] { 8 }, TiffColorType.WhiteIsZero8)]
[InlineData(true, TiffPhotometricInterpretation.WhiteIsZero, new[] { 8 }, TiffColorType.WhiteIsZero8)]
[InlineData(false, TiffPhotometricInterpretation.WhiteIsZero, new[] { 4 }, TiffColorType.WhiteIsZero4)]
[InlineData(true, TiffPhotometricInterpretation.WhiteIsZero, new[] { 4 }, TiffColorType.WhiteIsZero4)]
[InlineData(false, TiffPhotometricInterpretation.WhiteIsZero, new[] { 1 }, TiffColorType.WhiteIsZero1)]
[InlineData(true, TiffPhotometricInterpretation.WhiteIsZero, new[] { 1 }, TiffColorType.WhiteIsZero1)]
public void ReadImageFormat_DeterminesCorrectColorImplementation(bool isLittleEndian, ushort photometricInterpretation, int[] bitsPerSample, int colorType)
{
Stream stream = CreateTiffGenIfd()
@ -192,6 +196,23 @@ namespace ImageSharp.Tests
Assert.Equal((TiffColorType)colorType, decoder.ColorType);
}
[Theory]
[InlineData(false, TiffPhotometricInterpretation.WhiteIsZero, TiffColorType.WhiteIsZero1)]
[InlineData(true, TiffPhotometricInterpretation.WhiteIsZero, TiffColorType.WhiteIsZero1)]
public void ReadImageFormat_DeterminesCorrectColorImplementation_DefaultsToBilevel(bool isLittleEndian, ushort photometricInterpretation, int colorType)
{
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((TiffColorType)colorType, decoder.ColorType);
}
// [Theory]
// [InlineData(false, new[] { 8 }, TiffColorType.WhiteIsZero8)]
// [InlineData(true, new[] { 8 }, TiffColorType.WhiteIsZero8)]
@ -285,9 +306,14 @@ namespace ImageSharp.Tests
[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)
{
TiffDecoderCore decoder = new TiffDecoderCore(null, null);
decoder.ColorType = (TiffColorType)colorType;
int bufferSize = decoder.CalculateImageBufferSize(width, height);

Loading…
Cancel
Save