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 Rgb4_Data + { + get + { + yield return new object[] { Rgb4_Bytes4x4, new[] { 4u, 4u, 4u }, 0, 0, 4, 4, Rgb4_Result4x4 }; + yield return new object[] { Rgb4_Bytes4x4, new[] { 4u, 4u, 4u }, 0, 0, 4, 4, Offset(Rgb4_Result4x4, 0, 0, 6, 6) }; + yield return new object[] { Rgb4_Bytes4x4, new[] { 4u, 4u, 4u }, 1, 0, 4, 4, Offset(Rgb4_Result4x4, 1, 0, 6, 6) }; + yield return new object[] { Rgb4_Bytes4x4, new[] { 4u, 4u, 4u }, 0, 1, 4, 4, Offset(Rgb4_Result4x4, 0, 1, 6, 6) }; + yield return new object[] { Rgb4_Bytes4x4, new[] { 4u, 4u, 4u }, 1, 1, 4, 4, Offset(Rgb4_Result4x4, 1, 1, 6, 6) }; + + yield return new object[] { Rgb4_Bytes3x4, new[] { 4u, 4u, 4u }, 0, 0, 3, 4, Rgb4_Result3x4 }; + yield return new object[] { Rgb4_Bytes3x4, new[] { 4u, 4u, 4u }, 0, 0, 3, 4, Offset(Rgb4_Result3x4, 0, 0, 6, 6) }; + yield return new object[] { Rgb4_Bytes3x4, new[] { 4u, 4u, 4u }, 1, 0, 3, 4, Offset(Rgb4_Result3x4, 1, 0, 6, 6) }; + yield return new object[] { Rgb4_Bytes3x4, new[] { 4u, 4u, 4u }, 0, 1, 3, 4, Offset(Rgb4_Result3x4, 0, 1, 6, 6) }; + yield return new object[] { Rgb4_Bytes3x4, new[] { 4u, 4u, 4u }, 1, 1, 3, 4, Offset(Rgb4_Result3x4, 1, 1, 6, 6) }; + } + } + + private static Rgba32 Rgb8_000 = new Rgba32(0, 0, 0, 255); + private static Rgba32 Rgb8_444 = new Rgba32(64, 64, 64, 255); + private static Rgba32 Rgb8_888 = new Rgba32(128, 128, 128, 255); + private static Rgba32 Rgb8_CCC = new Rgba32(192, 192, 192, 255); + private static Rgba32 Rgb8_FFF = new Rgba32(255, 255, 255, 255); + private static Rgba32 Rgb8_F00 = new Rgba32(255, 0, 0, 255); + private static Rgba32 Rgb8_0F0 = new Rgba32(0, 255, 0, 255); + private static Rgba32 Rgb8_00F = new Rgba32(0, 0, 255, 255); + private static Rgba32 Rgb8_F0F = new Rgba32(255, 0, 255, 255); + private static Rgba32 Rgb8_400 = new Rgba32(64, 0, 0, 255); + private static Rgba32 Rgb8_800 = new Rgba32(128, 0, 0, 255); + private static Rgba32 Rgb8_C00 = new Rgba32(192, 0, 0, 255); + private static Rgba32 Rgb8_48C = new Rgba32(64, 128, 192, 255); + + private static byte[] Rgb8_Bytes4x4_R = new byte[] { 000, 255, 000, 255, + 255, 000, 000, 255, + 064, 128, 192, 064, + 000, 064, 128, 192 }; + + private static byte[] Rgb8_Bytes4x4_G = new byte[] { 000, 255, 000, 255, + 000, 255, 000, 000, + 000, 000, 000, 128, + 000, 064, 128, 192 }; + + private static byte[] Rgb8_Bytes4x4_B = new byte[] { 000, 255, 000, 255, + 000, 000, 255, 255, + 000, 000, 000, 192, + 000, 064, 128, 192 }; + + private static byte[][] Rgb8_Bytes4x4 = new[] { Rgb8_Bytes4x4_R, Rgb8_Bytes4x4_G, Rgb8_Bytes4x4_B }; + + private static Rgba32[][] Rgb8_Result4x4 = new[] { new[] { Rgb8_000, Rgb8_FFF, Rgb8_000, Rgb8_FFF }, + new[] { Rgb8_F00, Rgb8_0F0, Rgb8_00F, Rgb8_F0F }, + new[] { Rgb8_400, Rgb8_800, Rgb8_C00, Rgb8_48C }, + new[] { Rgb8_000, Rgb8_444, Rgb8_888, Rgb8_CCC }}; + + public static IEnumerable Rgb8_Data + { + get + { + yield return new object[] { Rgb8_Bytes4x4, new[] { 8u, 8u, 8u }, 0, 0, 4, 4, Rgb8_Result4x4 }; + yield return new object[] { Rgb8_Bytes4x4, new[] { 8u, 8u, 8u }, 0, 0, 4, 4, Offset(Rgb8_Result4x4, 0, 0, 6, 6) }; + yield return new object[] { Rgb8_Bytes4x4, new[] { 8u, 8u, 8u }, 1, 0, 4, 4, Offset(Rgb8_Result4x4, 1, 0, 6, 6) }; + yield return new object[] { Rgb8_Bytes4x4, new[] { 8u, 8u, 8u }, 0, 1, 4, 4, Offset(Rgb8_Result4x4, 0, 1, 6, 6) }; + yield return new object[] { Rgb8_Bytes4x4, new[] { 8u, 8u, 8u }, 1, 1, 4, 4, Offset(Rgb8_Result4x4, 1, 1, 6, 6) }; + } + } + + private static Rgba32 Rgb484_000 = new Rgba32(0, 0, 0, 255); + private static Rgba32 Rgb484_444 = new Rgba32(68, 64, 68, 255); + private static Rgba32 Rgb484_888 = new Rgba32(136, 128, 136, 255); + private static Rgba32 Rgb484_CCC = new Rgba32(204, 192, 204, 255); + private static Rgba32 Rgb484_FFF = new Rgba32(255, 255, 255, 255); + private static Rgba32 Rgb484_F00 = new Rgba32(255, 0, 0, 255); + private static Rgba32 Rgb484_0F0 = new Rgba32(0, 255, 0, 255); + private static Rgba32 Rgb484_00F = new Rgba32(0, 0, 255, 255); + private static Rgba32 Rgb484_F0F = new Rgba32(255, 0, 255, 255); + private static Rgba32 Rgb484_400 = new Rgba32(68, 0, 0, 255); + private static Rgba32 Rgb484_800 = new Rgba32(136, 0, 0, 255); + private static Rgba32 Rgb484_C00 = new Rgba32(204, 0, 0, 255); + private static Rgba32 Rgb484_48C = new Rgba32(68, 128, 204, 255); + + private static byte[] Rgb484_Bytes4x4_R = new byte[] { 0x0F, 0x0F, + 0xF0, 0x0F, + 0x48, 0xC4, + 0x04, 0x8C }; + + private static byte[] Rgb484_Bytes4x4_G = new byte[] { 0x00, 0xFF, 0x00, 0xFF, + 0x00, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x80, + 0x00, 0x40, 0x80, 0xC0 }; + + private static byte[] Rgb484_Bytes4x4_B = new byte[] { 0x0F, 0x0F, + 0x00, 0xFF, + 0x00, 0x0C, + 0x04, 0x8C }; + + private static Rgba32[][] Rgb484_Result4x4 = new[] { new[] { Rgb484_000, Rgb484_FFF, Rgb484_000, Rgb484_FFF }, + new[] { Rgb484_F00, Rgb484_0F0, Rgb484_00F, Rgb484_F0F }, + new[] { Rgb484_400, Rgb484_800, Rgb484_C00, Rgb484_48C }, + new[] { Rgb484_000, Rgb484_444, Rgb484_888, Rgb484_CCC }}; + + private static byte[][] Rgb484_Bytes4x4 = new[] { Rgb484_Bytes4x4_R, Rgb484_Bytes4x4_G, Rgb484_Bytes4x4_B }; + + public static IEnumerable Rgb484_Data + { + get + { + yield return new object[] { Rgb484_Bytes4x4, new[] { 4u, 8u, 4u }, 0, 0, 4, 4, Rgb484_Result4x4 }; + yield return new object[] { Rgb484_Bytes4x4, new[] { 4u, 8u, 4u }, 0, 0, 4, 4, Offset(Rgb484_Result4x4, 0, 0, 6, 6) }; + yield return new object[] { Rgb484_Bytes4x4, new[] { 4u, 8u, 4u }, 1, 0, 4, 4, Offset(Rgb484_Result4x4, 1, 0, 6, 6) }; + yield return new object[] { Rgb484_Bytes4x4, new[] { 4u, 8u, 4u }, 0, 1, 4, 4, Offset(Rgb484_Result4x4, 0, 1, 6, 6) }; + yield return new object[] { Rgb484_Bytes4x4, new[] { 4u, 8u, 4u }, 1, 1, 4, 4, Offset(Rgb484_Result4x4, 1, 1, 6, 6) }; + } + } + + [Theory] + [MemberData(nameof(Rgb4_Data))] + [MemberData(nameof(Rgb8_Data))] + [MemberData(nameof(Rgb484_Data))] + public void Decode_WritesPixelData(byte[][] inputData, uint[] bitsPerSample, int left, int top, int width, int height, Rgba32[][] expectedResult) + { + AssertDecode(expectedResult, pixels => + { + RgbPlanarTiffColor.Decode(inputData, bitsPerSample, pixels, left, top, width, height); + }); + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs index 6eef305ff..c779a631e 100644 --- a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs +++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs @@ -204,10 +204,31 @@ namespace ImageSharp.Tests [InlineData(true, TiffPhotometricInterpretation.Rgb, new[] { 4, 4, 4 }, TiffColorType.Rgb)] [InlineData(false, TiffPhotometricInterpretation.Rgb, new[] { 8, 8, 8 }, TiffColorType.Rgb888)] [InlineData(true, TiffPhotometricInterpretation.Rgb, new[] { 8, 8, 8 }, TiffColorType.Rgb888)] - public void ReadImageFormat_DeterminesCorrectColorImplementation(bool isLittleEndian, ushort photometricInterpretation, int[] bitsPerSample, int colorType) + public void ReadImageFormat_DeterminesCorrectColorImplementation_Chunky(bool isLittleEndian, ushort photometricInterpretation, int[] bitsPerSample, int colorType) { Stream stream = CreateTiffGenIfd() .WithEntry(TiffGenEntry.Integer(TiffTags.PhotometricInterpretation, TiffType.Short, photometricInterpretation)) + .WithEntry(TiffGenEntry.Integer(TiffTags.PlanarConfiguration, TiffType.Short, (int)TiffPlanarConfiguration.Chunky)) + .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); + + Assert.Equal((TiffColorType)colorType, decoder.ColorType); + } + + [Theory] + [InlineData(false, TiffPhotometricInterpretation.Rgb, new[] { 4, 4, 4 }, TiffColorType.RgbPlanar)] + [InlineData(true, TiffPhotometricInterpretation.Rgb, new[] { 4, 4, 4 }, TiffColorType.RgbPlanar)] + [InlineData(false, TiffPhotometricInterpretation.Rgb, new[] { 8, 8, 8 }, TiffColorType.RgbPlanar)] + [InlineData(true, TiffPhotometricInterpretation.Rgb, new[] { 8, 8, 8 }, TiffColorType.RgbPlanar)] + public void ReadImageFormat_DeterminesCorrectColorImplementation_Planar(bool isLittleEndian, ushort photometricInterpretation, int[] bitsPerSample, int colorType) + { + Stream stream = CreateTiffGenIfd() + .WithEntry(TiffGenEntry.Integer(TiffTags.PhotometricInterpretation, TiffType.Short, photometricInterpretation)) + .WithEntry(TiffGenEntry.Integer(TiffTags.PlanarConfiguration, TiffType.Short, (int)TiffPlanarConfiguration.Planar)) .WithEntry(TiffGenEntry.Integer(TiffTags.BitsPerSample, TiffType.Short, bitsPerSample)) .ToStream(isLittleEndian); @@ -431,6 +452,43 @@ namespace ImageSharp.Tests Assert.Equal("The TIFF ColorMap entry is missing for a pallete color image.", e.Message); } + [Theory] + [InlineData(false, TiffPlanarConfiguration.Chunky)] + [InlineData(true, TiffPlanarConfiguration.Chunky)] + [InlineData(false, TiffPlanarConfiguration.Planar)] + [InlineData(true, TiffPlanarConfiguration.Planar)] + public void ReadImageFormat_ReadsPlanarConfiguration(bool isLittleEndian, int planarConfiguration) + { + Stream stream = CreateTiffGenIfd() + .WithEntry(TiffGenEntry.Integer(TiffTags.PhotometricInterpretation, TiffType.Short, (int)TiffPhotometricInterpretation.Rgb)) + .WithEntry(TiffGenEntry.Integer(TiffTags.BitsPerSample, TiffType.Short, new int[] { 8, 8, 8 })) + .WithEntry(TiffGenEntry.Integer(TiffTags.PlanarConfiguration, TiffType.Short, (int)planarConfiguration)) + .ToStream(isLittleEndian); + + TiffDecoderCore decoder = new TiffDecoderCore(stream, isLittleEndian, null, null); + TiffIfd ifd = decoder.ReadIfd(0); + decoder.ReadImageFormat(ifd); + + Assert.Equal((TiffPlanarConfiguration)planarConfiguration, decoder.PlanarConfiguration); + } + + [Theory] + [MemberData(nameof(IsLittleEndianValues))] + public void ReadImageFormat_DefaultsPlanarConfigurationToChunky(bool isLittleEndian) + { + Stream stream = CreateTiffGenIfd() + .WithEntry(TiffGenEntry.Integer(TiffTags.PhotometricInterpretation, TiffType.Short, (int)TiffPhotometricInterpretation.Rgb)) + .WithEntry(TiffGenEntry.Integer(TiffTags.BitsPerSample, TiffType.Short, new int[] { 8, 8, 8 })) + .WithoutEntry(TiffTags.PlanarConfiguration) + .ToStream(isLittleEndian); + + TiffDecoderCore decoder = new TiffDecoderCore(stream, isLittleEndian, null, null); + TiffIfd ifd = decoder.ReadIfd(0); + decoder.ReadImageFormat(ifd); + + Assert.Equal(TiffPlanarConfiguration.Chunky, decoder.PlanarConfiguration); + } + [Theory] [InlineData(TiffColorType.WhiteIsZero, new uint[] { 1 }, 160, 80, 20 * 80)] [InlineData(TiffColorType.WhiteIsZero, new uint[] { 1 }, 153, 80, 20 * 80)] @@ -438,22 +496,49 @@ namespace ImageSharp.Tests [InlineData(TiffColorType.WhiteIsZero, new uint[] { 4 }, 100, 80, 50 * 80)] [InlineData(TiffColorType.WhiteIsZero, new uint[] { 4 }, 99, 80, 50 * 80)] [InlineData(TiffColorType.WhiteIsZero, new uint[] { 8 }, 100, 80, 100 * 80)] - [InlineData(TiffColorType.PaletteColor, new uint[] { 1 }, 160, 80, 60 * 80)] - [InlineData(TiffColorType.PaletteColor, new uint[] { 1 }, 153, 80, 58 * 80)] - [InlineData(TiffColorType.PaletteColor, new uint[] { 3 }, 100, 80, 113 * 80)] - [InlineData(TiffColorType.PaletteColor, new uint[] { 4 }, 100, 80, 150 * 80)] - [InlineData(TiffColorType.PaletteColor, new uint[] { 4 }, 99, 80, 149 * 80)] - [InlineData(TiffColorType.PaletteColor, new uint[] { 8 }, 100, 80, 300 * 80)] + [InlineData(TiffColorType.PaletteColor, new uint[] { 1 }, 160, 80, 20 * 80)] + [InlineData(TiffColorType.PaletteColor, new uint[] { 1 }, 153, 80, 20 * 80)] + [InlineData(TiffColorType.PaletteColor, new uint[] { 3 }, 100, 80, 38 * 80)] + [InlineData(TiffColorType.PaletteColor, new uint[] { 4 }, 100, 80, 50 * 80)] + [InlineData(TiffColorType.PaletteColor, new uint[] { 4 }, 99, 80, 50 * 80)] + [InlineData(TiffColorType.PaletteColor, new uint[] { 8 }, 100, 80, 100 * 80)] [InlineData(TiffColorType.Rgb, new uint[] { 8, 8, 8 }, 100, 80, 300 * 80)] [InlineData(TiffColorType.Rgb, new uint[] { 4, 4, 4 }, 100, 80, 150 * 80)] [InlineData(TiffColorType.Rgb, new uint[] { 4, 8, 4 }, 100, 80, 200 * 80)] - public void CalculateImageBufferSize_ReturnsCorrectSize(ushort colorType, uint[] bitsPerSample, int width, int height, int expectedResult) + public void CalculateImageBufferSize_ReturnsCorrectSize_Chunky(ushort colorType, uint[] bitsPerSample, int width, int height, int expectedResult) + { + TiffDecoderCore decoder = new TiffDecoderCore(null, null); + decoder.ColorType = (TiffColorType)colorType; + decoder.PlanarConfiguration = TiffPlanarConfiguration.Chunky; + decoder.BitsPerSample = bitsPerSample; + + int bufferSize = decoder.CalculateImageBufferSize(width, height, 0); + + Assert.Equal(expectedResult, bufferSize); + } + + [Theory] + [InlineData(TiffColorType.Rgb, new uint[] { 8, 8, 8 }, 100, 80, 0, 100 * 80)] + [InlineData(TiffColorType.Rgb, new uint[] { 8, 8, 8 }, 100, 80, 1, 100 * 80)] + [InlineData(TiffColorType.Rgb, new uint[] { 8, 8, 8 }, 100, 80, 2, 100 * 80)] + [InlineData(TiffColorType.Rgb, new uint[] { 4, 4, 4 }, 100, 80, 0, 50 * 80)] + [InlineData(TiffColorType.Rgb, new uint[] { 4, 4, 4 }, 100, 80, 1, 50 * 80)] + [InlineData(TiffColorType.Rgb, new uint[] { 4, 4, 4 }, 100, 80, 2, 50 * 80)] + [InlineData(TiffColorType.Rgb, new uint[] { 4, 8, 4 }, 100, 80, 0, 50 * 80)] + [InlineData(TiffColorType.Rgb, new uint[] { 4, 8, 4 }, 100, 80, 1, 100 * 80)] + [InlineData(TiffColorType.Rgb, new uint[] { 4, 8, 4 }, 100, 80, 2, 50 * 80)] + [InlineData(TiffColorType.Rgb, new uint[] { 4, 8, 4 }, 99, 80, 0, 50 * 80)] + [InlineData(TiffColorType.Rgb, new uint[] { 4, 8, 4 }, 99, 80, 1, 99 * 80)] + [InlineData(TiffColorType.Rgb, new uint[] { 4, 8, 4 }, 99, 80, 2, 50 * 80)] + + public void CalculateImageBufferSize_ReturnsCorrectSize_Planar(ushort colorType, uint[] bitsPerSample, int width, int height, int plane, int expectedResult) { TiffDecoderCore decoder = new TiffDecoderCore(null, null); decoder.ColorType = (TiffColorType)colorType; + decoder.PlanarConfiguration = TiffPlanarConfiguration.Planar; decoder.BitsPerSample = bitsPerSample; - int bufferSize = decoder.CalculateImageBufferSize(width, height); + int bufferSize = decoder.CalculateImageBufferSize(width, height, plane); Assert.Equal(expectedResult, bufferSize); } diff --git a/tests/ImageSharp.Formats.Tiff.Tests/ImageSharp.Formats.Tiff.Tests.csproj b/tests/ImageSharp.Formats.Tiff.Tests/ImageSharp.Formats.Tiff.Tests.csproj index dae9c9db3..3e861f778 100644 --- a/tests/ImageSharp.Formats.Tiff.Tests/ImageSharp.Formats.Tiff.Tests.csproj +++ b/tests/ImageSharp.Formats.Tiff.Tests/ImageSharp.Formats.Tiff.Tests.csproj @@ -14,6 +14,7 @@ +