From 4cbb28ba62e115ec274ee16c4c6c85266e404c40 Mon Sep 17 00:00:00 2001 From: Andrew Wilkinson Date: Mon, 13 Mar 2017 20:32:22 +0000 Subject: [PATCH] Read floating-point numbers from TIFF IFDs --- src/ImageSharp.Formats.Tiff/TiffConstants.cs | 10 + .../TiffDecoderCore.cs | 69 ++++++ .../Formats/Tiff/TiffDecoderIfdEntryTests.cs | 217 ++++++++++++++++++ 3 files changed, 296 insertions(+) diff --git a/src/ImageSharp.Formats.Tiff/TiffConstants.cs b/src/ImageSharp.Formats.Tiff/TiffConstants.cs index 84b90e69fe..858a1ca4bc 100644 --- a/src/ImageSharp.Formats.Tiff/TiffConstants.cs +++ b/src/ImageSharp.Formats.Tiff/TiffConstants.cs @@ -51,5 +51,15 @@ namespace ImageSharp.Formats /// Size (in bytes) of the Rational and SRational data types /// public const int SizeOfRational = 8; + + /// + /// Size (in bytes) of the Float data type + /// + public const int SizeOfFloat = 4; + + /// + /// Size (in bytes) of the Double data type + /// + public const int SizeOfDouble = 8; } } diff --git a/src/ImageSharp.Formats.Tiff/TiffDecoderCore.cs b/src/ImageSharp.Formats.Tiff/TiffDecoderCore.cs index 7d5bcf5198..1c2703ed57 100644 --- a/src/ImageSharp.Formats.Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp.Formats.Tiff/TiffDecoderCore.cs @@ -316,6 +316,53 @@ namespace ImageSharp.Formats return result; } + public float ReadFloat(ref TiffIfdEntry entry) + { + if (entry.Count != 1) + throw new ImageFormatException($"Cannot read a single value from an array of multiple items."); + + if (entry.Type != TiffType.Float) + throw new ImageFormatException($"A value of type '{entry.Type}' cannot be converted to a float."); + + return ToSingle(entry.Value, 0); + } + + public double ReadDouble(ref TiffIfdEntry entry) + { + if (entry.Count != 1) + throw new ImageFormatException($"Cannot read a single value from an array of multiple items."); + + return ReadDoubleArray(ref entry)[0]; + } + + public float[] ReadFloatArray(ref TiffIfdEntry entry) + { + if (entry.Type != TiffType.Float) + throw new ImageFormatException($"A value of type '{entry.Type}' cannot be converted to a float."); + + byte[] bytes = ReadBytes(ref entry); + float[] result = new float[entry.Count]; + + for (int i = 0 ; i < result.Length ; i++) + result[i] = ToSingle(bytes, i * TiffConstants.SizeOfFloat); + + return result; + } + + public double[] ReadDoubleArray(ref TiffIfdEntry entry) + { + if (entry.Type != TiffType.Double) + throw new ImageFormatException($"A value of type '{entry.Type}' cannot be converted to a double."); + + byte[] bytes = ReadBytes(ref entry); + double[] result = new double[entry.Count]; + + for (int i = 0 ; i < result.Length ; i++) + result[i] = ToDouble(bytes, i * TiffConstants.SizeOfDouble); + + return result; + } + private SByte ToSByte(byte[] bytes, int offset) { return (sbyte)bytes[offset]; @@ -352,6 +399,28 @@ namespace ImageSharp.Formats return (ushort)ToInt16(bytes, offset); } + private Single ToSingle(byte[] bytes, int offset) + { + byte[] buffer = new byte[4]; + Array.Copy(bytes, offset, buffer, 0, 4); + + if (BitConverter.IsLittleEndian != IsLittleEndian) + Array.Reverse(buffer); + + return BitConverter.ToSingle(buffer, 0); + } + + private Double ToDouble(byte[] bytes, int offset) + { + byte[] buffer = new byte[8]; + Array.Copy(bytes, offset, buffer, 0, 8); + + if (BitConverter.IsLittleEndian != IsLittleEndian) + Array.Reverse(buffer); + + return BitConverter.ToDouble(buffer, 0); + } + public static uint GetSizeOfData(TiffIfdEntry entry) => SizeOfDataType(entry.Type) * entry.Count; private static uint SizeOfDataType(TiffType type) diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderIfdEntryTests.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderIfdEntryTests.cs index b810182c98..369fc61de1 100644 --- a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderIfdEntryTests.cs +++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderIfdEntryTests.cs @@ -607,6 +607,223 @@ namespace ImageSharp.Tests Assert.Equal($"Cannot read a single value from an array of multiple items.", e.Message); } + [Theory] + [InlineDataAttribute(false, new byte[] { 0x00, 0x00, 0x00, 0x00 }, 0.0F)] + [InlineDataAttribute(false, new byte[] { 0x3F, 0x80, 0x00, 0x00 }, 1.0F)] + [InlineDataAttribute(false, new byte[] { 0xC0, 0x00, 0x00, 0x00 }, -2.0F)] + [InlineDataAttribute(false, new byte[] { 0x7F, 0x7F, 0xFF, 0xFF }, float.MaxValue)] + [InlineDataAttribute(false, new byte[] { 0x7F, 0x80, 0x00, 0x00 }, float.PositiveInfinity)] + [InlineDataAttribute(false, new byte[] { 0xFF, 0x80, 0x00, 0x00 }, float.NegativeInfinity)] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x00, 0x00 }, 0.0F)] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x80, 0x3F }, 1.0F)] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x00, 0xC0 }, -2.0F)] + [InlineDataAttribute(true, new byte[] { 0xFF, 0xFF, 0x7F, 0x7F }, float.MaxValue)] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x80, 0x7F }, float.PositiveInfinity)] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x80, 0xFF }, float.NegativeInfinity)] + public void ReadFloat_ReturnsValue(bool isLittleEndian, byte[] bytes, float expectedValue) + { + (TiffDecoderCore decoder, TiffIfdEntry entry) = GenerateTestIfdEntry(TiffGenEntry.Bytes(TiffTags.ImageWidth, TiffType.Float, 1, bytes), isLittleEndian); + + float result = decoder.ReadFloat(ref entry); + + Assert.Equal(expectedValue, result); + } + + [Theory] + [InlineDataAttribute(TiffType.Byte)] + [InlineDataAttribute(TiffType.Ascii)] + [InlineDataAttribute(TiffType.Short)] + [InlineDataAttribute(TiffType.Long)] + [InlineDataAttribute(TiffType.Rational)] + [InlineDataAttribute(TiffType.SByte)] + [InlineDataAttribute(TiffType.Undefined)] + [InlineDataAttribute(TiffType.SShort)] + [InlineDataAttribute(TiffType.SLong)] + [InlineDataAttribute(TiffType.SRational)] + [InlineDataAttribute(TiffType.Double)] + [InlineDataAttribute(TiffType.Ifd)] + [InlineDataAttribute((TiffType)99)] + public void ReadFloat_ThrowsExceptionIfInvalidType(ushort type) + { + (TiffDecoderCore decoder, TiffIfdEntry entry) = GenerateTestIfdEntry(TiffGenEntry.Bytes(TiffTags.ImageWidth, (TiffType)type, 1, new byte[4]), true); + + var e = Assert.Throws(() => decoder.ReadFloat(ref entry)); + + Assert.Equal($"A value of type '{(TiffType)type}' cannot be converted to a float.", e.Message); + } + + [Theory] + [InlineDataAttribute(false)] + [InlineDataAttribute(true)] + public void ReadFloat_ThrowsExceptionIfCountIsNotOne(bool isLittleEndian) + { + (TiffDecoderCore decoder, TiffIfdEntry entry) = GenerateTestIfdEntry(TiffGenEntry.Bytes(TiffTags.ImageWidth, TiffType.Float, 2, new byte[4]), isLittleEndian); + + var e = Assert.Throws(() => decoder.ReadFloat(ref entry)); + + Assert.Equal($"Cannot read a single value from an array of multiple items.", e.Message); + } + + [Theory] + [InlineDataAttribute(false, new byte[] { 0x00, 0x00, 0x00, 0x00 }, new float[] { 0.0F })] + [InlineDataAttribute(false, new byte[] { 0x3F, 0x80, 0x00, 0x00 }, new float[] { 1.0F })] + [InlineDataAttribute(false, new byte[] { 0xC0, 0x00, 0x00, 0x00 }, new float[] { -2.0F })] + [InlineDataAttribute(false, new byte[] { 0x7F, 0x7F, 0xFF, 0xFF }, new float[] { float.MaxValue })] + [InlineDataAttribute(false, new byte[] { 0x7F, 0x80, 0x00, 0x00 }, new float[] { float.PositiveInfinity })] + [InlineDataAttribute(false, new byte[] { 0xFF, 0x80, 0x00, 0x00 }, new float[] { float.NegativeInfinity })] + [InlineDataAttribute(false, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00 }, new float[] { 0.0F, 1.0F, -2.0F })] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x00, 0x00 }, new float[] { 0.0F })] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x80, 0x3F }, new float[] { 1.0F })] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x00, 0xC0 }, new float[] { -2.0F })] + [InlineDataAttribute(true, new byte[] { 0xFF, 0xFF, 0x7F, 0x7F }, new float[] { float.MaxValue })] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x80, 0x7F }, new float[] { float.PositiveInfinity })] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x80, 0xFF }, new float[] { float.NegativeInfinity })] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xC0 }, new float[] { 0.0F, 1.0F, -2.0F })] + + public void ReadFloatArray_ReturnsValue(bool isLittleEndian, byte[] bytes, float[] expectedValue) + { + (TiffDecoderCore decoder, TiffIfdEntry entry) = GenerateTestIfdEntry(TiffGenEntry.Bytes(TiffTags.ImageWidth, TiffType.Float, (uint)expectedValue.Length, bytes), isLittleEndian); + + float[] result = decoder.ReadFloatArray(ref entry); + + Assert.Equal(expectedValue, result); + } + + [Theory] + [InlineDataAttribute(TiffType.Byte)] + [InlineDataAttribute(TiffType.Ascii)] + [InlineDataAttribute(TiffType.Short)] + [InlineDataAttribute(TiffType.Long)] + [InlineDataAttribute(TiffType.Rational)] + [InlineDataAttribute(TiffType.SByte)] + [InlineDataAttribute(TiffType.Undefined)] + [InlineDataAttribute(TiffType.SShort)] + [InlineDataAttribute(TiffType.SLong)] + [InlineDataAttribute(TiffType.SRational)] + [InlineDataAttribute(TiffType.Double)] + [InlineDataAttribute(TiffType.Ifd)] + [InlineDataAttribute((TiffType)99)] + public void ReadFloatArray_ThrowsExceptionIfInvalidType(ushort type) + { + (TiffDecoderCore decoder, TiffIfdEntry entry) = GenerateTestIfdEntry(TiffGenEntry.Bytes(TiffTags.ImageWidth, (TiffType)type, 1, new byte[4]), true); + + var e = Assert.Throws(() => decoder.ReadFloatArray(ref entry)); + + Assert.Equal($"A value of type '{(TiffType)type}' cannot be converted to a float.", e.Message); + } + + [Theory] + [InlineDataAttribute(false, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, 0.0)] + [InlineDataAttribute(false, new byte[] { 0x3F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, 1.0)] + [InlineDataAttribute(false, new byte[] { 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, 2.0)] + [InlineDataAttribute(false, new byte[] { 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, -2.0)] + [InlineDataAttribute(false, new byte[] { 0x7F, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, double.MaxValue)] + [InlineDataAttribute(false, new byte[] { 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, double.PositiveInfinity)] + [InlineDataAttribute(false, new byte[] { 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, double.NegativeInfinity)] + [InlineDataAttribute(false, new byte[] { 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, double.NaN)] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, 0.0)] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F }, 1.0)] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40 }, 2.0)] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0 }, -2.0)] + [InlineDataAttribute(true, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0x7F }, double.MaxValue)] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x7F }, double.PositiveInfinity)] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF }, double.NegativeInfinity)] + [InlineDataAttribute(true, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F }, double.NaN)] + public void ReadDouble_ReturnsValue(bool isLittleEndian, byte[] bytes, double expectedValue) + { + (TiffDecoderCore decoder, TiffIfdEntry entry) = GenerateTestIfdEntry(TiffGenEntry.Bytes(TiffTags.ImageWidth, TiffType.Double, 1, bytes), isLittleEndian); + + double result = decoder.ReadDouble(ref entry); + + Assert.Equal(expectedValue, result); + } + + [Theory] + [InlineDataAttribute(TiffType.Byte)] + [InlineDataAttribute(TiffType.Ascii)] + [InlineDataAttribute(TiffType.Short)] + [InlineDataAttribute(TiffType.Long)] + [InlineDataAttribute(TiffType.Rational)] + [InlineDataAttribute(TiffType.SByte)] + [InlineDataAttribute(TiffType.Undefined)] + [InlineDataAttribute(TiffType.SShort)] + [InlineDataAttribute(TiffType.SLong)] + [InlineDataAttribute(TiffType.SRational)] + [InlineDataAttribute(TiffType.Float)] + [InlineDataAttribute(TiffType.Ifd)] + [InlineDataAttribute((TiffType)99)] + public void ReadDouble_ThrowsExceptionIfInvalidType(ushort type) + { + (TiffDecoderCore decoder, TiffIfdEntry entry) = GenerateTestIfdEntry(TiffGenEntry.Bytes(TiffTags.ImageWidth, (TiffType)type, 1, new byte[4]), true); + + var e = Assert.Throws(() => decoder.ReadDouble(ref entry)); + + Assert.Equal($"A value of type '{(TiffType)type}' cannot be converted to a double.", e.Message); + } + + [Theory] + [InlineDataAttribute(false)] + [InlineDataAttribute(true)] + public void ReadDouble_ThrowsExceptionIfCountIsNotOne(bool isLittleEndian) + { + (TiffDecoderCore decoder, TiffIfdEntry entry) = GenerateTestIfdEntry(TiffGenEntry.Bytes(TiffTags.ImageWidth, TiffType.Double, 2, new byte[4]), isLittleEndian); + + var e = Assert.Throws(() => decoder.ReadDouble(ref entry)); + + Assert.Equal($"Cannot read a single value from an array of multiple items.", e.Message); + } + + [Theory] + [InlineDataAttribute(false, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, new double[] { 0.0 })] + [InlineDataAttribute(false, new byte[] { 0x3F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, new double[] { 1.0 })] + [InlineDataAttribute(false, new byte[] { 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, new double[] { 2.0 })] + [InlineDataAttribute(false, new byte[] { 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, new double[] { -2.0 })] + [InlineDataAttribute(false, new byte[] { 0x7F, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, new double[] { double.MaxValue })] + [InlineDataAttribute(false, new byte[] { 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, new double[] { double.PositiveInfinity })] + [InlineDataAttribute(false, new byte[] { 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, new double[] { double.NegativeInfinity })] + [InlineDataAttribute(false, new byte[] { 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, new double[] { double.NaN })] + [InlineDataAttribute(false, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, new double[] { 0.0, 1.0, -2.0 })] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, new double[] { 0.0 })] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F }, new double[] { 1.0 })] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40 }, new double[] { 2.0 })] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0 }, new double[] { -2.0 })] + [InlineDataAttribute(true, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0x7F }, new double[] { double.MaxValue })] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x7F }, new double[] { double.PositiveInfinity })] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF }, new double[] { double.NegativeInfinity })] + [InlineDataAttribute(true, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F }, new double[] { double.NaN })] + [InlineDataAttribute(true, new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0 }, new double[] { 0.0, 1.0, -2.0 })] + public void ReadDoubleArray_ReturnsValue(bool isLittleEndian, byte[] bytes, double[] expectedValue) + { + (TiffDecoderCore decoder, TiffIfdEntry entry) = GenerateTestIfdEntry(TiffGenEntry.Bytes(TiffTags.ImageWidth, TiffType.Double, (uint)expectedValue.Length, bytes), isLittleEndian); + + double[] result = decoder.ReadDoubleArray(ref entry); + + Assert.Equal(expectedValue, result); + } + + [Theory] + [InlineDataAttribute(TiffType.Byte)] + [InlineDataAttribute(TiffType.Ascii)] + [InlineDataAttribute(TiffType.Short)] + [InlineDataAttribute(TiffType.Long)] + [InlineDataAttribute(TiffType.Rational)] + [InlineDataAttribute(TiffType.SByte)] + [InlineDataAttribute(TiffType.Undefined)] + [InlineDataAttribute(TiffType.SShort)] + [InlineDataAttribute(TiffType.SLong)] + [InlineDataAttribute(TiffType.SRational)] + [InlineDataAttribute(TiffType.Float)] + [InlineDataAttribute(TiffType.Ifd)] + [InlineDataAttribute((TiffType)99)] + public void ReadDoubleArray_ThrowsExceptionIfInvalidType(ushort type) + { + (TiffDecoderCore decoder, TiffIfdEntry entry) = GenerateTestIfdEntry(TiffGenEntry.Bytes(TiffTags.ImageWidth, (TiffType)type, 1, new byte[4]), true); + + var e = Assert.Throws(() => decoder.ReadDoubleArray(ref entry)); + + Assert.Equal($"A value of type '{(TiffType)type}' cannot be converted to a double.", e.Message); + } + private (TiffDecoderCore, TiffIfdEntry) GenerateTestIfdEntry(TiffGenEntry entry, bool isLittleEndian) { Stream stream = new TiffGenIfd()