diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor.cs
new file mode 100644
index 000000000..bcd8e171b
--- /dev/null
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor.cs
@@ -0,0 +1,60 @@
+//
+// 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 'RGB' photometric interpretation with 'Planar' layout (for all bit depths).
+ ///
+ internal static class RgbPlanarTiffColor
+ {
+ ///
+ /// Decodes pixel data using the current photometric interpretation.
+ ///
+ /// The pixel format.
+ /// The buffers 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 rBitReader = new BitReader(data[0]);
+ BitReader gBitReader = new BitReader(data[1]);
+ BitReader bBitReader = new BitReader(data[2]);
+ float rFactor = (float)Math.Pow(2, bitsPerSample[0]) - 1.0f;
+ float gFactor = (float)Math.Pow(2, bitsPerSample[1]) - 1.0f;
+ float bFactor = (float)Math.Pow(2, bitsPerSample[2]) - 1.0f;
+
+ for (int y = top; y < top + height; y++)
+ {
+ for (int x = left; x < left + width; x++)
+ {
+ float r = ((float)rBitReader.ReadBits(bitsPerSample[0])) / rFactor;
+ float g = ((float)gBitReader.ReadBits(bitsPerSample[1])) / gFactor;
+ float b = ((float)bBitReader.ReadBits(bitsPerSample[2])) / bFactor;
+ color.PackFromVector4(new Vector4(r, g, b, 1.0f));
+ pixels[x, y] = color;
+ }
+
+ rBitReader.NextRow();
+ gBitReader.NextRow();
+ bBitReader.NextRow();
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs
index 630696b77..36e00edf4 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs
@@ -63,6 +63,11 @@ namespace ImageSharp.Formats.Tiff
///
/// RGB Full Color. Optimised implementation for 8-bit images.
///
- Rgb888
+ Rgb888,
+
+ ///
+ /// RGB Full Color. Planar configuration of data.
+ ///
+ RgbPlanar,
}
}
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
index e7c98cad7..a2d1f37c8 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
@@ -82,6 +82,11 @@ namespace ImageSharp.Formats
///
public bool IsLittleEndian { get; private set; }
+ ///
+ /// Gets or sets the planar configuration type to use when decoding the image.
+ ///
+ public TiffPlanarConfiguration PlanarConfiguration { get; set; }
+
///
/// Calculates the size (in bytes) of the data contained within an IFD entry.
///
@@ -281,6 +286,15 @@ namespace ImageSharp.Formats
}
}
+ if (ifd.TryGetIfdEntry(TiffTags.PlanarConfiguration, out TiffIfdEntry planarConfigurationEntry))
+ {
+ this.PlanarConfiguration = (TiffPlanarConfiguration)this.ReadUnsignedInteger(ref planarConfigurationEntry);
+ }
+ else
+ {
+ this.PlanarConfiguration = TiffPlanarConfiguration.Chunky;
+ }
+
TiffPhotometricInterpretation photometricInterpretation;
if (ifd.TryGetIfdEntry(TiffTags.PhotometricInterpretation, out TiffIfdEntry photometricInterpretationEntry))
@@ -400,13 +414,20 @@ namespace ImageSharp.Formats
{
if (this.BitsPerSample.Length == 3)
{
- if (this.BitsPerSample[0] == 8 && this.BitsPerSample[1] == 8 && this.BitsPerSample[2] == 8)
+ if (this.PlanarConfiguration == TiffPlanarConfiguration.Chunky)
{
- this.ColorType = TiffColorType.Rgb888;
+ if (this.BitsPerSample[0] == 8 && this.BitsPerSample[1] == 8 && this.BitsPerSample[2] == 8)
+ {
+ this.ColorType = TiffColorType.Rgb888;
+ }
+ else
+ {
+ this.ColorType = TiffColorType.Rgb;
+ }
}
else
{
- this.ColorType = TiffColorType.Rgb;
+ this.ColorType = TiffColorType.RgbPlanar;
}
}
else
@@ -457,17 +478,25 @@ namespace ImageSharp.Formats
///
/// The width for the desired pixel buffer.
/// The height for the desired pixel buffer.
+ /// The index of the plane for planar image configuration (or zero for chunky).
/// The size (in bytes) of the required pixel buffer.
- public int CalculateImageBufferSize(int width, int height)
+ public int CalculateImageBufferSize(int width, int height, int plane)
{
uint bitsPerPixel = 0;
- for (int i = 0; i < this.BitsPerSample.Length; i++)
+
+ if (this.PlanarConfiguration == TiffPlanarConfiguration.Chunky)
{
- bitsPerPixel += this.BitsPerSample[i];
+ for (int i = 0; i < this.BitsPerSample.Length; i++)
+ {
+ bitsPerPixel += this.BitsPerSample[i];
+ }
+ }
+ else
+ {
+ bitsPerPixel = this.BitsPerSample[plane];
}
- int sampleMultiplier = this.ColorType == TiffColorType.PaletteColor ? 3 : 1;
- int bytesPerRow = ((width * (int)bitsPerPixel * sampleMultiplier) + 7) / 8;
+ int bytesPerRow = ((width * (int)bitsPerPixel) + 7) / 8;
return bytesPerRow * height;
}
@@ -498,7 +527,7 @@ namespace ImageSharp.Formats
}
///
- /// Decodes pixel data using the current photometric interpretation.
+ /// Decodes pixel data using the current photometric interpretation (chunky configuration).
///
/// The pixel format.
/// The buffer to read image data from.
@@ -507,7 +536,7 @@ namespace ImageSharp.Formats
/// 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)
+ public void ProcessImageBlockChunky(byte[] data, PixelAccessor pixels, int left, int top, int width, int height)
where TPixel : struct, IPixel
{
switch (this.ColorType)
@@ -550,6 +579,29 @@ namespace ImageSharp.Formats
}
}
+ ///
+ /// Decodes pixel data using the current photometric interpretation (planar configuration).
+ ///
+ /// 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 ProcessImageBlockPlanar(byte[][] data, PixelAccessor pixels, int left, int top, int width, int height)
+ where TPixel : struct, IPixel
+ {
+ switch (this.ColorType)
+ {
+ case TiffColorType.RgbPlanar:
+ RgbPlanarTiffColor.Decode(data, this.BitsPerSample, pixels, left, top, width, height);
+ break;
+ default:
+ throw new InvalidOperationException();
+ }
+ }
+
///
/// Reads the data from a as an array of bytes.
///
@@ -1108,25 +1160,47 @@ namespace ImageSharp.Formats
private void DecodeImageStrips(Image image, int rowsPerStrip, uint[] stripOffsets, uint[] stripByteCounts)
where TPixel : struct, IPixel
{
- int uncompressedStripSize = this.CalculateImageBufferSize(image.Width, rowsPerStrip);
+ int stripsPerPixel = this.PlanarConfiguration == TiffPlanarConfiguration.Chunky ? 1 : this.BitsPerSample.Length;
+ int stripsPerPlane = stripOffsets.Length / stripsPerPixel;
using (PixelAccessor pixels = image.Lock())
{
- byte[] stripBytes = ArrayPool.Shared.Rent(uncompressedStripSize);
+ byte[][] stripBytes = new byte[stripsPerPixel][];
+
+ for (int stripIndex = 0; stripIndex < stripBytes.Length; stripIndex++)
+ {
+ int uncompressedStripSize = this.CalculateImageBufferSize(image.Width, rowsPerStrip, stripIndex);
+ stripBytes[stripIndex] = ArrayPool.Shared.Rent(uncompressedStripSize);
+ }
try
{
- for (int i = 0; i < stripOffsets.Length; i++)
+ for (int i = 0; i < stripsPerPlane; i++)
{
- int stripHeight = i < stripOffsets.Length - 1 || image.Height % rowsPerStrip == 0 ? rowsPerStrip : image.Height % rowsPerStrip;
+ int stripHeight = i < stripsPerPlane - 1 || image.Height % rowsPerStrip == 0 ? rowsPerStrip : image.Height % rowsPerStrip;
+
+ for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++)
+ {
+ int stripIndex = i * stripsPerPixel + planeIndex;
+ this.DecompressImageBlock(stripOffsets[stripIndex], stripByteCounts[stripIndex], stripBytes[planeIndex]);
+ }
- this.DecompressImageBlock(stripOffsets[i], stripByteCounts[i], stripBytes);
- this.ProcessImageBlock(stripBytes, pixels, 0, rowsPerStrip * i, image.Width, stripHeight);
+ if (this.PlanarConfiguration == TiffPlanarConfiguration.Chunky)
+ {
+ this.ProcessImageBlockChunky(stripBytes[0], pixels, 0, rowsPerStrip * i, image.Width, stripHeight);
+ }
+ else
+ {
+ this.ProcessImageBlockPlanar(stripBytes, pixels, 0, rowsPerStrip * i, image.Width, stripHeight);
+ }
}
}
finally
{
- ArrayPool.Shared.Return(stripBytes);
+ for (int stripIndex = 0; stripIndex < stripBytes.Length; stripIndex++)
+ {
+ ArrayPool.Shared.Return(stripBytes[stripIndex]);
+ }
}
}
}
diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/PhotometricInterpretationTestBase.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/PhotometricInterpretationTestBase.cs
index ab9a89116..c07c37832 100644
--- a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/PhotometricInterpretationTestBase.cs
+++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/PhotometricInterpretationTestBase.cs
@@ -7,9 +7,12 @@ namespace ImageSharp.Tests
{
using System;
using Xunit;
+ using ImageSharp;
public abstract class PhotometricInterpretationTestBase
{
+ public static Rgba32 DefaultColor = new Rgba32(42, 96, 18, 128);
+
public static Rgba32[][] Offset(Rgba32[][] input, int xOffset, int yOffset, int width, int height)
{
int inputHeight = input.Length;
@@ -20,6 +23,11 @@ namespace ImageSharp.Tests
for (int y = 0; y < output.Length; y++)
{
output[y] = new Rgba32[width];
+
+ for (int x = 0; x < width; x++)
+ {
+ output[y][x] = DefaultColor;
+ }
}
for (int y = 0; y < inputHeight; y++)
@@ -38,6 +46,7 @@ namespace ImageSharp.Tests
int resultWidth = expectedResult[0].Length;
int resultHeight = expectedResult.Length;
Image image = new Image(resultWidth, resultHeight);
+ image.Fill(DefaultColor);
using (PixelAccessor pixels = image.Lock())
{
@@ -51,7 +60,7 @@ namespace ImageSharp.Tests
for (int x = 0; x < resultWidth; x++)
{
Assert.True(expectedResult[y][x] == pixels[x, y],
- $"Pixel ({x}, {y}) should be {expectedResult[y][x]} but was {pixels[x,y]}");
+ $"Pixel ({x}, {y}) should be {expectedResult[y][x]} but was {pixels[x, y]}");
}
}
}
diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColorTests.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColorTests.cs
new file mode 100644
index 000000000..2b06a8af5
--- /dev/null
+++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColorTests.cs
@@ -0,0 +1,199 @@
+//
+// 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 RgbPlanarTiffColorTests : PhotometricInterpretationTestBase
+ {
+ private static Rgba32 Rgb4_000 = new Rgba32(0, 0, 0, 255);
+ private static Rgba32 Rgb4_444 = new Rgba32(68, 68, 68, 255);
+ private static Rgba32 Rgb4_888 = new Rgba32(136, 136, 136, 255);
+ private static Rgba32 Rgb4_CCC = new Rgba32(204, 204, 204, 255);
+ private static Rgba32 Rgb4_FFF = new Rgba32(255, 255, 255, 255);
+ private static Rgba32 Rgb4_F00 = new Rgba32(255, 0, 0, 255);
+ private static Rgba32 Rgb4_0F0 = new Rgba32(0, 255, 0, 255);
+ private static Rgba32 Rgb4_00F = new Rgba32(0, 0, 255, 255);
+ private static Rgba32 Rgb4_F0F = new Rgba32(255, 0, 255, 255);
+ private static Rgba32 Rgb4_400 = new Rgba32(68, 0, 0, 255);
+ private static Rgba32 Rgb4_800 = new Rgba32(136, 0, 0, 255);
+ private static Rgba32 Rgb4_C00 = new Rgba32(204, 0, 0, 255);
+ private static Rgba32 Rgb4_48C = new Rgba32(68, 136, 204, 255);
+
+ private static byte[] Rgb4_Bytes4x4_R = new byte[] { 0x0F, 0x0F,
+ 0xF0, 0x0F,
+ 0x48, 0xC4,
+ 0x04, 0x8C };
+
+ private static byte[] Rgb4_Bytes4x4_G = new byte[] { 0x0F, 0x0F,
+ 0x0F, 0x00,
+ 0x00, 0x08,
+ 0x04, 0x8C };
+
+ private static byte[] Rgb4_Bytes4x4_B = new byte[] { 0x0F, 0x0F,
+ 0x00, 0xFF,
+ 0x00, 0x0C,
+ 0x04, 0x8C };
+
+ private static byte[][] Rgb4_Bytes4x4 = new[] { Rgb4_Bytes4x4_R, Rgb4_Bytes4x4_G, Rgb4_Bytes4x4_B };
+
+ private static Rgba32[][] Rgb4_Result4x4 = new[] { new[] { Rgb4_000, Rgb4_FFF, Rgb4_000, Rgb4_FFF },
+ new[] { Rgb4_F00, Rgb4_0F0, Rgb4_00F, Rgb4_F0F },
+ new[] { Rgb4_400, Rgb4_800, Rgb4_C00, Rgb4_48C },
+ new[] { Rgb4_000, Rgb4_444, Rgb4_888, Rgb4_CCC }};
+
+ private static byte[] Rgb4_Bytes3x4_R = new byte[] { 0x0F, 0x00,
+ 0xF0, 0x00,
+ 0x48, 0xC0,
+ 0x04, 0x80 };
+
+ private static byte[] Rgb4_Bytes3x4_G = new byte[] { 0x0F, 0x00,
+ 0x0F, 0x00,
+ 0x00, 0x00,
+ 0x04, 0x80 };
+
+ private static byte[] Rgb4_Bytes3x4_B = new byte[] { 0x0F, 0x00,
+ 0x00, 0xF0,
+ 0x00, 0x00,
+ 0x04, 0x80 };
+
+ private static byte[][] Rgb4_Bytes3x4 = new[] { Rgb4_Bytes3x4_R, Rgb4_Bytes3x4_G, Rgb4_Bytes3x4_B };
+
+ private static Rgba32[][] Rgb4_Result3x4 = new[] { new[] { Rgb4_000, Rgb4_FFF, Rgb4_000 },
+ new[] { Rgb4_F00, Rgb4_0F0, Rgb4_00F },
+ new[] { Rgb4_400, Rgb4_800, Rgb4_C00 },
+ new[] { Rgb4_000, Rgb4_444, Rgb4_888 }};
+
+ public static IEnumerable