From 143a73e655efcf864148e7e2fa2deea1ef097794 Mon Sep 17 00:00:00 2001 From: Andrew Wilkinson Date: Wed, 29 Mar 2017 20:44:20 +0100 Subject: [PATCH] Decode TIFF image resolution --- .../Tiff/Constants/TiffResolutionUnit.cs | 2 +- .../Formats/Tiff/TiffDecoderCore.cs | 20 +++++ .../Formats/Tiff/TiffIfd/TiffIfd.cs | 24 ++++-- .../Formats/Tiff/TiffDecoderImageTests.cs | 76 ++++++++++++++++--- .../Formats/Tiff/TiffIfd/TiffIfdTests.cs | 35 +++++++++ .../TestUtilities/Tiff/TiffGenEntry.cs | 30 +++++++- .../Tiff/TiffGenIfdExtensions.cs | 15 +++- 7 files changed, 180 insertions(+), 22 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffResolutionUnit.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffResolutionUnit.cs index 582c476443..307f9b9d23 100644 --- a/src/ImageSharp/Formats/Tiff/Constants/TiffResolutionUnit.cs +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffResolutionUnit.cs @@ -23,6 +23,6 @@ namespace ImageSharp.Formats /// /// Centimeter. /// - Centimeter = 2 + Centimeter = 3 } } \ No newline at end of file diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index eb4c6b1c89..28d45a6e1f 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -170,6 +170,26 @@ namespace ImageSharp.Formats int height = (int)this.ReadUnsignedInteger(ref imageLengthEntry); image.InitPixels(width, height); + + TiffResolutionUnit resolutionUnit = TiffResolutionUnit.Inch; + if (ifd.TryGetIfdEntry(TiffTags.ResolutionUnit, out TiffIfdEntry resolutionUnitEntry)) + { + resolutionUnit = (TiffResolutionUnit)this.ReadUnsignedInteger(ref resolutionUnitEntry); + } + + double resolutionUnitFactor = resolutionUnit == TiffResolutionUnit.Centimeter ? 1.0 / 2.54 : 1.0; + + if (ifd.TryGetIfdEntry(TiffTags.XResolution, out TiffIfdEntry xResolutionEntry)) + { + Rational xResolution = this.ReadUnsignedRational(ref xResolutionEntry); + image.MetaData.HorizontalResolution = xResolution.ToDouble(); + } + + if (ifd.TryGetIfdEntry(TiffTags.YResolution, out TiffIfdEntry yResolutionEntry)) + { + Rational yResolution = this.ReadUnsignedRational(ref yResolutionEntry); + image.MetaData.VerticalResolution = yResolution.ToDouble(); + } } /// diff --git a/src/ImageSharp/Formats/Tiff/TiffIfd/TiffIfd.cs b/src/ImageSharp/Formats/Tiff/TiffIfd/TiffIfd.cs index 2206e97f39..d2071aff9c 100644 --- a/src/ImageSharp/Formats/Tiff/TiffIfd/TiffIfd.cs +++ b/src/ImageSharp/Formats/Tiff/TiffIfd/TiffIfd.cs @@ -35,21 +35,31 @@ namespace ImageSharp.Formats /// Gets the child with the specified tag ID. /// /// The tag ID to search for. - /// The resulting , if it exists. - /// A flag indicating whether the requested entry exists - public bool TryGetIfdEntry(ushort tag, out TiffIfdEntry entry) + /// The resulting , or null if it does not exists. + public TiffIfdEntry? GetIfdEntry(ushort tag) { for (int i = 0; i < this.Entries.Length; i++) { if (this.Entries[i].Tag == tag) { - entry = this.Entries[i]; - return true; + return this.Entries[i]; } } - entry = default(TiffIfdEntry); - return false; + return null; + } + + /// + /// Gets the child with the specified tag ID. + /// + /// The tag ID to search for. + /// The resulting , if it exists. + /// A flag indicating whether the requested entry exists. + public bool TryGetIfdEntry(ushort tag, out TiffIfdEntry entry) + { + TiffIfdEntry? nullableEntry = this.GetIfdEntry(tag); + entry = nullableEntry ?? default(TiffIfdEntry); + return nullableEntry.HasValue; } } } diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs index 824bbc3b5b..b0a97102fc 100644 --- a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs +++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffDecoderImageTests.cs @@ -14,6 +14,8 @@ namespace ImageSharp.Tests { public const int ImageWidth = 200; public const int ImageHeight = 150; + public const int XResolution = 100; + public const int YResolution = 200; public static object[][] IsLittleEndianValues = new[] { new object[] { false }, new object[] { true } }; @@ -27,14 +29,67 @@ namespace ImageSharp.Tests TiffDecoderCore decoder = new TiffDecoderCore(stream, isLittleEndian, null); TiffIfd ifd = decoder.ReadIfd(0); - Image image = new Image(1,1); + Image image = new Image(1, 1); decoder.DecodeImage(ifd, image); - + Assert.Equal(ImageWidth, image.Width); Assert.Equal(ImageHeight, image.Height); } + [Theory] + [InlineData(false, 150u, 1u, 200u, 1u, 2u /* Inch */, 150.0, 200.0)] + [InlineData(false, 150u, 1u, 200u, 1u, 3u /* Cm */, 150.0 / 2.54, 200.0 / 2.54)] + [InlineData(false, 150u, 1u, 200u, 1u, 1u /* None */, 96.0, 96.0)] + [InlineData(false, 150u, 1u, 200u, 1u, null /* Inch */, 150.0, 200.0)] + [InlineData(false, 5u, 2u, 9u, 4u, 2u /* Inch */, 2.5, 2.25)] + [InlineData(false, null, null, null, null, null /* Inch */, 96.0, 96.0)] + [InlineData(false, 150u, 1u, null, null, 2u /* Inch */, 150.0, 96.0)] + [InlineData(false, null, null, 200u, 1u, 2u /* Inch */, 96.0, 200.0)] + [InlineData(true, 150u, 1u, 200u, 1u, 2u /* Inch */, 150.0, 200.0)] + [InlineData(true, 150u, 1u, 200u, 1u, 3u /* Cm */, 150.0 / 2.54, 200.0 / 2.54)] + [InlineData(true, 150u, 1u, 200u, 1u, 1u /* None */, 96.0, 96.0)] + [InlineData(false, 5u, 2u, 9u, 4u, 2u /* Inch */, 2.5, 2.25)] + [InlineData(true, 150u, 1u, 200u, 1u, null /* Inch */, 150.0, 200.0)] + [InlineData(true, null, null, null, null, null /* Inch */, 96.0, 96.0)] + [InlineData(true, 150u, 1u, null, null, 2u /* Inch */, 150.0, 96.0)] + [InlineData(true, null, null, 200u, 1u, 2u /* Inch */, 96.0, 200.0)] + public void DecodeImage_SetsImageResolution(bool isLittleEndian, uint? xResolutionNumerator, uint? xResolutionDenominator, + uint? yResolutionNumerator, uint? yResolutionDenominator, uint? resolutionUnit, + double expectedHorizonalResolution, double expectedVerticalResolution) + { + TiffGenIfd ifdGen = CreateTiffGenIfd() + .WithoutEntry(TiffTags.XResolution) + .WithoutEntry(TiffTags.YResolution) + .WithoutEntry(TiffTags.ResolutionUnit); + + if (xResolutionNumerator != null) + { + ifdGen.WithEntry(TiffGenEntry.Rational(TiffTags.XResolution, xResolutionNumerator.Value, xResolutionDenominator.Value)); + } + + if (yResolutionNumerator != null) + { + ifdGen.WithEntry(TiffGenEntry.Rational(TiffTags.YResolution, yResolutionNumerator.Value, yResolutionDenominator.Value)); + } + + if (resolutionUnit != null) + { + ifdGen.WithEntry(TiffGenEntry.Integer(TiffTags.ResolutionUnit, TiffType.Short, resolutionUnit.Value)); + } + + Stream stream = ifdGen.ToStream(isLittleEndian); + + TiffDecoderCore decoder = new TiffDecoderCore(stream, isLittleEndian, null); + TiffIfd ifd = decoder.ReadIfd(0); + Image image = new Image(1, 1); + + decoder.DecodeImage(ifd, image); + + Assert.Equal(expectedHorizonalResolution, image.MetaData.HorizontalResolution); + Assert.Equal(expectedVerticalResolution, image.MetaData.VerticalResolution); + } + [Theory] [MemberData(nameof(IsLittleEndianValues))] public void DecodeImage_ThrowsException_WithMissingImageWidth(bool isLittleEndian) @@ -45,8 +100,8 @@ namespace ImageSharp.Tests TiffDecoderCore decoder = new TiffDecoderCore(stream, isLittleEndian, null); TiffIfd ifd = decoder.ReadIfd(0); - Image image = new Image(1,1); - + Image image = new Image(1, 1); + var e = Assert.Throws(() => decoder.DecodeImage(ifd, image)); Assert.Equal("The TIFF IFD does not specify the image dimensions.", e.Message); @@ -62,8 +117,8 @@ namespace ImageSharp.Tests TiffDecoderCore decoder = new TiffDecoderCore(stream, isLittleEndian, null); TiffIfd ifd = decoder.ReadIfd(0); - Image image = new Image(1,1); - + Image image = new Image(1, 1); + var e = Assert.Throws(() => decoder.DecodeImage(ifd, image)); Assert.Equal("The TIFF IFD does not specify the image dimensions.", e.Message); @@ -72,13 +127,16 @@ namespace ImageSharp.Tests private TiffGenIfd CreateTiffGenIfd() { return new TiffGenIfd() - { - Entries = + { + Entries = { TiffGenEntry.Integer(TiffTags.ImageWidth, TiffType.Long, ImageWidth), TiffGenEntry.Integer(TiffTags.ImageLength, TiffType.Long, ImageHeight), + TiffGenEntry.Rational(TiffTags.XResolution, XResolution, 1), + TiffGenEntry.Rational(TiffTags.YResolution, YResolution, 1), + TiffGenEntry.Integer(TiffTags.ResolutionUnit, TiffType.Short, 2) } - }; + }; } } } \ No newline at end of file diff --git a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffIfd/TiffIfdTests.cs b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffIfd/TiffIfdTests.cs index d9f8425cb6..c680475390 100644 --- a/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffIfd/TiffIfdTests.cs +++ b/tests/ImageSharp.Formats.Tiff.Tests/Formats/Tiff/TiffIfd/TiffIfdTests.cs @@ -22,6 +22,41 @@ namespace ImageSharp.Tests Assert.Equal(1234u, ifd.NextIfdOffset); } + [Fact] + public void GetIfdEntry_ReturnsIfdIfExists() + { + var entries = new[] + { + new TiffIfdEntry(10, TiffType.Short, 20, new byte[4]), + new TiffIfdEntry(20, TiffType.Short, 20, new byte[4]), + new TiffIfdEntry(30, TiffType.Short, 20, new byte[4]), + new TiffIfdEntry(40, TiffType.Short, 20, new byte[4]) + }; + var ifd = new TiffIfd(entries, 1234u); + + TiffIfdEntry? entry = ifd.GetIfdEntry(30); + + Assert.Equal(true, entry.HasValue); + Assert.Equal(30, entry.Value.Tag); + } + + [Fact] + public void GetIfdEntry_ReturnsNullOtherwise() + { + var entries = new[] + { + new TiffIfdEntry(10, TiffType.Short, 20, new byte[4]), + new TiffIfdEntry(20, TiffType.Short, 20, new byte[4]), + new TiffIfdEntry(30, TiffType.Short, 20, new byte[4]), + new TiffIfdEntry(40, TiffType.Short, 20, new byte[4]) + }; + var ifd = new TiffIfd(entries, 1234u); + + TiffIfdEntry? entry = ifd.GetIfdEntry(25); + + Assert.Equal(false, entry.HasValue); + } + [Fact] public void TryGetIfdEntry_ReturnsIfdIfExists() { diff --git a/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenEntry.cs b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenEntry.cs index c0bb9d78c8..2065e8501a 100644 --- a/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenEntry.cs +++ b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenEntry.cs @@ -41,7 +41,7 @@ namespace ImageSharp.Tests public static TiffGenEntry Integer(ushort tag, TiffType type, int value) { - return TiffGenEntry.Integer(tag, type, new int[] {value}); + return TiffGenEntry.Integer(tag, type, new int[] { value }); } public static TiffGenEntry Integer(ushort tag, TiffType type, int[] value) @@ -55,7 +55,7 @@ namespace ImageSharp.Tests public static TiffGenEntry Integer(ushort tag, TiffType type, uint value) { - return TiffGenEntry.Integer(tag, type, new uint[] {value}); + return TiffGenEntry.Integer(tag, type, new uint[] { value }); } public static TiffGenEntry Integer(ushort tag, TiffType type, uint[] value) @@ -67,6 +67,11 @@ namespace ImageSharp.Tests return new TiffGenEntryUnsignedInteger(tag, type, value); } + public static TiffGenEntry Rational(ushort tag, uint numerator, uint denominator) + { + return new TiffGenEntryRational(tag, numerator, denominator); + } + private class TiffGenEntryAscii : TiffGenEntry { public TiffGenEntryAscii(ushort tag, string value) : base(tag, TiffType.Ascii, (uint)GetBytes(value).Length) @@ -176,5 +181,26 @@ namespace ImageSharp.Tests } } } + + private class TiffGenEntryRational : TiffGenEntry + { + public TiffGenEntryRational(ushort tag, uint numerator, uint denominator) : base(tag, TiffType.Rational, 1u) + { + this.Numerator = numerator; + this.Denominator = denominator; + } + + public uint Numerator { get; } + + public uint Denominator { get; } + + public override IEnumerable GetData(bool isLittleEndian) + { + byte[] numeratorBytes = BitConverter.GetBytes(Numerator).WithByteOrder(isLittleEndian); + byte[] denominatorBytes = BitConverter.GetBytes(Denominator).WithByteOrder(isLittleEndian); + byte[] bytes = Enumerable.Concat(numeratorBytes, denominatorBytes).ToArray(); + return new[] { new TiffGenDataBlock(bytes) }; + } + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenIfdExtensions.cs b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenIfdExtensions.cs index 84ea0f1acd..c442916402 100644 --- a/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenIfdExtensions.cs +++ b/tests/ImageSharp.Formats.Tiff.Tests/TestUtilities/Tiff/TiffGenIfdExtensions.cs @@ -5,8 +5,6 @@ namespace ImageSharp.Tests { - using System; - using System.IO; using System.Linq; /// @@ -17,7 +15,18 @@ namespace ImageSharp.Tests public static TiffGenIfd WithoutEntry(this TiffGenIfd ifd, ushort tag) { TiffGenEntry entry = ifd.Entries.First(e => e.Tag == tag); - ifd.Entries.Remove(entry); + if (entry != null) + { + ifd.Entries.Remove(entry); + } + return ifd; + } + + public static TiffGenIfd WithEntry(this TiffGenIfd ifd, TiffGenEntry entry) + { + ifd.WithoutEntry(entry.Tag); + ifd.Entries.Add(entry); + return ifd; } }