From 713e26241ca5a4d0f2fbc5db00d3e712136fa476 Mon Sep 17 00:00:00 2001 From: Christian Fortmann Date: Tue, 3 Mar 2026 14:31:15 +0100 Subject: [PATCH 1/2] add test for pyramidal tiff with subifds --- UnitTests/PyramidTiffTests.cs | 111 ++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 UnitTests/PyramidTiffTests.cs diff --git a/UnitTests/PyramidTiffTests.cs b/UnitTests/PyramidTiffTests.cs new file mode 100644 index 0000000..67ee06c --- /dev/null +++ b/UnitTests/PyramidTiffTests.cs @@ -0,0 +1,111 @@ +using System; +using System.IO; +using BitMiracle.LibTiff.Classic; +using NUnit.Framework; + +namespace UnitTests +{ + public static class TestImages + { + public static byte[] CreateTileBytes(int tileSize, int channels) + { + // Size * size pixels * channels, interleaved + var arr = new byte[tileSize * tileSize * channels]; + for (int i = 0; i < arr.Length; i++) + arr[i] = (byte)(i % 256); + return arr; + } + } + + [TestFixture] + public class PyramidTiffTests + { + private const int TileSize = 256; + + [TestCase(1)] + [TestCase(2)] + public void CanCreateAndReadPyramidalTiffWithSubIfds_ParameterizedChannels(int channelCount) + { + string path = Path.Combine(TestContext.CurrentContext.WorkDirectory, + $"pyramid_test_{channelCount}ch.tif"); + + if (File.Exists(path)) + File.Delete(path); + + // ---------- WRITE ---------- + using (var tiff = Tiff.Open(path, "w")) + { + // IFD0 + WriteLevel(tiff, 1024, 1024, channelCount); + tiff.SetField(TiffTag.SUBIFD, 2, new long[2]); + tiff.WriteDirectory(); + + // SubIFD #1 + tiff.CreateDirectory(); + WriteLevel(tiff, 512, 512, channelCount); + tiff.WriteDirectory(); + + // SubIFD #2 + tiff.CreateDirectory(); + WriteLevel(tiff, 256, 256, channelCount); + tiff.WriteDirectory(); + } + + // ---------- READ ---------- + using var read = Tiff.Open(path, "r"); + + // IFD0 + Assert.AreEqual(1024, read.GetField(TiffTag.IMAGEWIDTH)[0].ToInt()); + Assert.AreEqual(1024, read.GetField(TiffTag.IMAGELENGTH)[0].ToInt()); + Assert.AreEqual(channelCount, read.GetField(TiffTag.SAMPLESPERPIXEL)[0].ToInt(), + "IFD0 channel count mismatch"); + + var field = read.GetField(TiffTag.SUBIFD); + Assert.NotNull(field, "Missing SUBIFD tag"); + + long[] subIfds = (long[])field[1].Value; + Assert.AreEqual(2, subIfds.Length); + + // SubIFD1 + read.SetSubDirectory(subIfds[0]); + Assert.AreEqual(512, read.GetField(TiffTag.IMAGEWIDTH)[0].ToInt()); + Assert.AreEqual(512, read.GetField(TiffTag.IMAGELENGTH)[0].ToInt()); + Assert.AreEqual(channelCount, read.GetField(TiffTag.SAMPLESPERPIXEL)[0].ToInt(), + "SubIFD1 channel count mismatch"); + + // SubIFD2 + read.SetSubDirectory(subIfds[1]); + Assert.AreEqual(256, read.GetField(TiffTag.IMAGEWIDTH)[0].ToInt()); + Assert.AreEqual(256, read.GetField(TiffTag.IMAGELENGTH)[0].ToInt()); + Assert.AreEqual(channelCount, read.GetField(TiffTag.SAMPLESPERPIXEL)[0].ToInt(), + "SubIFD2 channel count mismatch"); + } + + private static void WriteLevel(Tiff tiff, int width, int height, int channelCount) + { + tiff.SetField(TiffTag.IMAGEWIDTH, width); + tiff.SetField(TiffTag.IMAGELENGTH, height); + tiff.SetField(TiffTag.BITSPERSAMPLE, 8); + tiff.SetField(TiffTag.SAMPLESPERPIXEL, channelCount); + tiff.SetField(TiffTag.PHOTOMETRIC, Photometric.MINISBLACK); + tiff.SetField(TiffTag.PLANARCONFIG, PlanarConfig.CONTIG); + + tiff.SetField(TiffTag.TILEWIDTH, TileSize); + tiff.SetField(TiffTag.TILELENGTH, TileSize); + + int tilesX = (width + TileSize - 1) / TileSize; + int tilesY = (height + TileSize - 1) / TileSize; + + byte[] tile = TestImages.CreateTileBytes(TileSize, channelCount); + + for (int ty = 0; ty < tilesY; ty++) + { + for (int tx = 0; tx < tilesX; tx++) + { + int tileIndex = tiff.ComputeTile(tx * TileSize, ty * TileSize, 0, 0); + tiff.WriteEncodedTile(tileIndex, tile, tile.Length); + } + } + } + } +} \ No newline at end of file From 2fd768ecefe92fc58484af4f52c5f709ea0454ff Mon Sep 17 00:00:00 2001 From: Christian Fortmann Date: Tue, 3 Mar 2026 14:32:58 +0100 Subject: [PATCH 2/2] fixing issue with incorrect (sub-)ifd placement --- LibTiff/Internal/Tiff_DirWrite.cs | 85 ++++++++++++++----------------- 1 file changed, 39 insertions(+), 46 deletions(-) diff --git a/LibTiff/Internal/Tiff_DirWrite.cs b/LibTiff/Internal/Tiff_DirWrite.cs index 7100a13..61b5de3 100644 --- a/LibTiff/Internal/Tiff_DirWrite.cs +++ b/LibTiff/Internal/Tiff_DirWrite.cs @@ -39,6 +39,10 @@ partial class Tiff // extended to RewriteDirectory() etc. private ulong PenultimateDirectoryOffset { get; set; } + private ulong m_subifdArrayOffset = 0; // Dateioffset des SubIFD-Offsets-Arrays + private int m_subifdElemSize = 0; // 4 (Classic TIFF) oder 8 (BigTIFF) + private int m_subifdWriteIndex = 0; // 0 .. (td_nsubifd - 1) + private ulong insertData(TiffType type, int v) { int t = (int)type; @@ -137,7 +141,7 @@ private bool writeDirectory(bool done) // and link it into the existing directory structure. if (m_diroff == 0 && !linkDirectory()) return false; - + // Size the directory so that we can calculate offsets for the data // items that aren't kept in-place in each field. nfields = 0; @@ -165,6 +169,28 @@ private bool writeDirectory(bool done) m_curdir++; int dir = 0; + if (m_dir.td_nsubifd > 0) + { + m_subifdElemSize = (m_header.tiff_version == TIFF_BIGTIFF_VERSION) ? 8 : 4; + m_subifdWriteIndex = 0; + m_subifdArrayOffset = m_dataoff; + + ulong total = (ulong)m_dir.td_nsubifd * (ulong)m_subifdElemSize; + if (total > 0) + { + byte[] zeros = new byte[total]; + seekFile((long)m_subifdArrayOffset, SeekOrigin.Begin); + if (!writeOK(zeros, 0, (int)total)) + { + ErrorExt(this, m_clientdata, m_name, "Error reserving SubIFD offset array"); + return false; + } + + m_dataoff += (ulong)((total + 1UL) & ~1UL); // 2-Byte Alignment + seekFile((long)m_dataoff, SeekOrigin.Begin); + } + } + // Setup external form of directory entries and write data items. int[] fields = new int[FieldBit.SetLongs]; Buffer.BlockCopy(m_dir.td_fieldsset, 0, fields, 0, FieldBit.SetLongs * sizeof(int)); @@ -317,51 +343,18 @@ private bool writeDirectory(bool done) break; case FieldBit.SubIFD: data[dir].tdir_tag = fip.Tag; - data[dir].tdir_count = (int)m_dir.td_nsubifd; - - // Total hack: if this directory includes a SubIFD - // tag then force the next directories to be - // written as "sub directories" of this one. This - // is used to write things like thumbnails and - // image masks that one wants to keep out of the - // normal directory linkage access mechanism. + data[dir].tdir_count = m_dir.td_nsubifd; + data[dir].tdir_type = (m_header.tiff_version == TIFF_BIGTIFF_VERSION) + ? TiffType.IFD8 + : TiffType.LONG; + data[dir].tdir_offset = m_subifdArrayOffset; + if (data[dir].tdir_count > 0) { m_flags |= TiffFlags.INSUBIFD; m_nsubifd = (short)data[dir].tdir_count; - if (data[dir].tdir_count > 1) - { - m_subifdoff = data[dir].tdir_offset; - } - else - { - if ((m_flags & TiffFlags.ISBIGTIFF) == TiffFlags.ISBIGTIFF) - { - m_subifdoff = m_diroff + sizeof(long) + - (ulong)dir * (ulong)TiffDirEntry.SizeInBytes(m_header.tiff_version == TIFF_BIGTIFF_VERSION) + - sizeof(short) * 2 + sizeof(long); - } - else - { - m_subifdoff = m_diroff + sizeof(short) + - (ulong)dir * (ulong)TiffDirEntry.SizeInBytes(m_header.tiff_version == TIFF_BIGTIFF_VERSION) + - sizeof(short) * 2 + sizeof(int); - } - } } - if ((m_flags & TiffFlags.ISBIGTIFF) == TiffFlags.ISBIGTIFF) - { - data[dir].tdir_type = TiffType.IFD8; - if (!writeLong8Array(ref data[dir], m_dir.td_subifd)) - return false; - } - else - { - data[dir].tdir_type = TiffType.LONG; - if (!writeLongArray(ref data[dir], LongToInt(m_dir.td_subifd))) - return false; - } break; default: // XXX: Should be fixed and removed. @@ -1783,7 +1776,8 @@ private bool linkDirectory() if ((m_flags & TiffFlags.INSUBIFD) == TiffFlags.INSUBIFD) { - seekFile((long)m_subifdoff, SeekOrigin.Begin); + ulong target = m_subifdArrayOffset + (ulong)(m_subifdWriteIndex * m_subifdElemSize); + seekFile((long)target, SeekOrigin.Begin); if (!writeDirOffOK((long)diroff, m_header.tiff_version == TIFF_BIGTIFF_VERSION)) { ErrorExt(this, m_clientdata, module, @@ -1791,13 +1785,12 @@ private bool linkDirectory() return false; } - // Advance to the next SubIFD or, if this is the last one - // configured, revert back to the normal directory linkage. + // Advance to the next SubIFD slot or, if this is the last one, + // revert back to the normal directory linkage. + m_subifdWriteIndex++; --m_nsubifd; - if (m_nsubifd != 0) - m_subifdoff += sizeof(int); - else + if (m_nsubifd == 0) m_flags &= ~TiffFlags.INSUBIFD; return true;