From 7298ac0ee82cc679e0d66080fe7bc66f3fba086f Mon Sep 17 00:00:00 2001 From: Sawraz Date: Mon, 15 Jun 2026 12:14:46 +0600 Subject: [PATCH 1/2] DW-39: Add Support for large tiff streaming --- .../UnitTests/AnyBitmapFunctionality.cs | 42 +++ .../IronSoftware.Drawing.Common/AnyBitmap.cs | 251 +++++++++++++++--- 2 files changed, 260 insertions(+), 33 deletions(-) diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs index ef91a05..7d8225f 100644 --- a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs @@ -579,6 +579,48 @@ public void Try_UnLoad_Tiff_Image() Assert.Equal(2, anyBitmap.FrameCount); } + [FactWithAutomaticDisplayName] + public void FromTiffFile_StreamsMultiPageTiff_MatchesFromFile() + { + string tiffPath = GetRelativeFilePath("IRON-274-39065.tif"); + + // Baseline: the standard in-memory loader. + var expected = AnyBitmap.FromFile(tiffPath); + + // Streaming loader: the path used automatically for TIFF files > ~2 GB. + var streamed = AnyBitmap.FromTiffFile(tiffPath); + + streamed.FrameCount.Should().Be(expected.FrameCount); + + var expectedFrames = expected.GetAllFrames.ToList(); + var streamedFrames = streamed.GetAllFrames.ToList(); + streamedFrames.Count.Should().Be(expectedFrames.Count); + for (int i = 0; i < expectedFrames.Count; i++) + { + streamedFrames[i].Width.Should().Be(expectedFrames[i].Width); + streamedFrames[i].Height.Should().Be(expectedFrames[i].Height); + } + } + + [FactWithAutomaticDisplayName] + public void FromTiffFile_StreamsEveryPage_OfMultiPageTiff() + { + string tiffPath = GetRelativeFilePath("test_dw_10.tif"); + + var expected = AnyBitmap.FromFile(tiffPath); + var streamed = AnyBitmap.FromTiffFile(tiffPath); + + streamed.FrameCount.Should().Be(expected.FrameCount); + streamed.FrameCount.Should().BeGreaterThan(1); + } + + [FactWithAutomaticDisplayName] + public void FromTiffFile_MissingFile_ThrowsFileNotFound() + { + Action act = () => AnyBitmap.FromTiffFile(GetRelativeFilePath("does-not-exist-DW39.tiff")); + act.Should().Throw(); + } + [FactWithAutomaticDisplayName] public void Create_Multi_page_Tiff() { diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs index b323b3c..d652de4 100644 --- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs @@ -706,6 +706,14 @@ public AnyBitmap(AnyBitmap original, int width, int height) LoadAndResizeImage(original, width, height); } + /// + /// Private parameterless constructor used by factory methods that populate + /// the image after construction (e.g. ). + /// + private AnyBitmap() + { + } + /// /// Construct a new Bitmap from a file. /// @@ -714,7 +722,7 @@ public AnyBitmap(AnyBitmap original, int width, int height) /// public AnyBitmap(string file) { - LoadImage(File.ReadAllBytes(file), true); + LoadImageFromFile(file, true); } /// @@ -726,7 +734,7 @@ public AnyBitmap(string file) /// public AnyBitmap(string file, bool preserveOriginalFormat) { - LoadImage(File.ReadAllBytes(file), preserveOriginalFormat); + LoadImageFromFile(file, preserveOriginalFormat); } /// @@ -876,6 +884,34 @@ public static AnyBitmap FromFile(string file, bool preserveOriginalFormat) } } + /// + /// Creates a new from a TIFF file by streaming it + /// from disk one page at a time. + /// + /// A fully qualified path to a TIFF file. + /// + /// Unlike , the file is never read into a + /// single byte[] buffer, so multi-page TIFF files larger than the + /// .NET ~2 GB single-array limit can be loaded natively without external + /// splitting. Each individual page must still fit within a single decode + /// buffer (see the page-size limit applied while decoding). + /// automatically falls back to this loader when a TIFF file exceeds the + /// in-memory size limit, so calling it explicitly is only required when you + /// want to force page-by-page streaming regardless of file size. + /// + /// + public static AnyBitmap FromTiffFile(string file) + { + if (!File.Exists(file)) + { + throw new FileNotFoundException($"TIFF file not found at path '{file}'.", file); + } + + AnyBitmap bitmap = new(); + bitmap.LoadLargeTiffFromFile(file); + return bitmap; + } + /// /// Construct a new Bitmap from a Uri. /// @@ -2845,59 +2881,208 @@ private Lazy> OpenTiffToImageSharp() } private IReadOnlyList InternalLoadTiff() + { + // Decode a TIFF that already lives in the in-memory Binary buffer. + using MemoryStream tiffStream = new(Binary); + + // Disable warning messages + Tiff.SetErrorHandler(new DisableErrorHandler()); + + // open a TIFF stored in the stream + using Tiff tiff = Tiff.ClientOpen("in-memory", "r", tiffStream, new TiffStream()); + if (tiff == null) + { + throw new NotSupportedException("Could not read image"); + } + + return ReadTiffFrames(tiff); + } + + /// + /// Reads every (non-thumbnail) directory of an already-open + /// into a list of ImageSharp images, decoding one page at a time. + /// + /// + /// The supplied may be backed by any stream - an + /// in-memory buffer or a . When it is backed by a + /// file, LibTiff seeks to each directory on demand, so the whole file is + /// never buffered. This is what allows multi-page TIFF files larger than + /// 2 GB to be loaded: total file size is unbounded, only an individual + /// page must fit within a single decode buffer. + /// + private List ReadTiffFrames(Tiff tiff) { int imageWidth = 0; int imageHeight = 0; double imageXResolution = 0; double imageYResolution = 0; - //IEnumerable images = new(); - // create a memory stream out of them - using MemoryStream tiffStream = new(Binary); + SetTiffCompression(tiff); - // Disable warning messages - Tiff.SetErrorHandler(new DisableErrorHandler()); List images = new(); - // open a TIFF stored in the stream - using (Tiff tiff = Tiff.ClientOpen("in-memory", "r", tiffStream, new TiffStream())) + short num = tiff.NumberOfDirectories(); + for (short i = 0; i < num; i++) { - SetTiffCompression(tiff); + _ = tiff.SetDirectory(i); - short num = tiff.NumberOfDirectories(); - for (short i = 0; i < num; i++) + if (IsThumbnail(tiff)) { - _ = tiff.SetDirectory(i); + continue; + } - if (IsThumbnail(tiff)) - { - continue; - } + var (width, height, horizontalResolution, verticalResolution) = SetWidthHeight(tiff, i, ref imageWidth, ref imageHeight, ref imageXResolution, ref imageYResolution); - var (width, height, horizontalResolution, verticalResolution) = SetWidthHeight(tiff, i, ref imageWidth, ref imageHeight, ref imageXResolution, ref imageYResolution); + // A single page is still decoded into one RGBA buffer, so its pixel + // count is bounded by the .NET single-array index limit. Multi-page + // files of any total size are fine as long as each page fits. + long pixelCount = (long)width * height; + if (pixelCount > MaxSingleFrameRasterPixels) + { + throw new NotSupportedException( + $"TIFF page {i} is {width}x{height} ({pixelCount:N0} pixels), which exceeds the maximum of " + + $"{MaxSingleFrameRasterPixels:N0} pixels that can be decoded into a single buffer. " + + "Split this page into smaller images before loading."); + } - // Read the image into the memory buffer - int[] raster = new int[height * width]; - if (!tiff.ReadRGBAImage(width, height, raster)) - { - throw new NotSupportedException("Could not read image"); - } + // Read the image into the memory buffer + int[] raster = new int[height * width]; + if (!tiff.ReadRGBAImage(width, height, raster)) + { + throw new NotSupportedException("Could not read image"); + } - var bits = PrepareByteArray(raster, width, height, 32); - - var image = Image.LoadPixelData(bits, width, height); + var bits = PrepareByteArray(raster, width, height, 32); - image.Metadata.HorizontalResolution = horizontalResolution; - image.Metadata.VerticalResolution = verticalResolution; - images.Add(image); + var image = Image.LoadPixelData(bits, width, height); - //Note1: it might be some case that the bytes of current Image is smaller/bigger than the original tiff - //Note2: 'yield return' make it super slow - } + image.Metadata.HorizontalResolution = horizontalResolution; + image.Metadata.VerticalResolution = verticalResolution; + images.Add(image); + //Note1: it might be some case that the bytes of current Image is smaller/bigger than the original tiff + //Note2: 'yield return' make it super slow } + return images; } + // .NET indexes arrays with a 32-bit integer, so a single byte[] (and + // therefore File.ReadAllBytes) cannot exceed ~2 GB. Files above this + // threshold are routed to a streaming loader for TIFF, or rejected with a + // clear message for formats that have no page-based streaming decoder. + private const long MaxInMemoryFileBytes = 2_000_000_000L; + + // An Rgba32 page is decoded into one byte[] of width*height*4 bytes; a + // single .NET array is capped at int.MaxValue bytes, so a page may hold at + // most ~536M pixels regardless of how large the overall file is. + private const long MaxSingleFrameRasterPixels = int.MaxValue / 4; + + /// + /// Loads an image from a file, transparently handling files that are too + /// large to fit in a single in-memory buffer. Large multi-page TIFF files + /// are streamed page-by-page from disk; other oversized formats raise a + /// clear, actionable exception instead of the opaque .NET array-size error. + /// + private void LoadImageFromFile(string file, bool preserveOriginalFormat) + { + long length; + try + { + length = new FileInfo(file).Length; + } + catch + { + // Surface the real access/IO error from the read below. + length = 0; + } + + if (length > MaxInMemoryFileBytes) + { + if (IsTiffFile(file)) + { + // Stream the TIFF page-by-page; never materialise the whole file. + LoadLargeTiffFromFile(file); + return; + } + + throw new NotSupportedException( + $"The image file '{file}' is {length:N0} bytes, which exceeds the ~2 GB limit for loading an " + + "image into a single memory buffer. Large multi-page TIFF files are supported via streaming; " + + "other formats must be split into smaller files before loading."); + } + + LoadImage(File.ReadAllBytes(file), preserveOriginalFormat); + } + + /// + /// Lightweight TIFF detection that reads only the 4-byte file header, + /// avoiding any full-file read. Recognises both classic TIFF (version 42, + /// 0x2A) and BigTIFF (version 43, 0x2B) in little-endian (II) and big-endian + /// (MM) byte order. BigTIFF detection is essential here because it is the + /// format typically used for the multi-gigabyte files this loader targets. + /// + private static bool IsTiffFile(string file) + { + try + { + using FileStream fs = new(file, FileMode.Open, FileAccess.Read, FileShare.Read); + byte[] header = new byte[4]; + int read = fs.Read(header, 0, 4); + return read == 4 && + ((header[0] == 0x49 && header[1] == 0x49 && header[2] == 0x2A && header[3] == 0x00) || // II classic + (header[0] == 0x4D && header[1] == 0x4D && header[2] == 0x00 && header[3] == 0x2A) || // MM classic + (header[0] == 0x49 && header[1] == 0x49 && header[2] == 0x2B && header[3] == 0x00) || // II BigTIFF + (header[0] == 0x4D && header[1] == 0x4D && header[2] == 0x00 && header[3] == 0x2B)); // MM BigTIFF + } + catch + { + return false; + } + } + + /// + /// Loads a TIFF that is too large to fit in a single in-memory buffer by + /// streaming it directly from disk. LibTiff reads one directory (page) at a + /// time through the underlying , so the entire file + /// is never allocated as one array, enabling TIFF files larger than 2 GB. + /// + private void LoadLargeTiffFromFile(string file) + { + Tiff.SetErrorHandler(new DisableErrorHandler()); + + List frames; + using (FileStream fileStream = new(file, FileMode.Open, FileAccess.Read, FileShare.Read, 1 << 20)) + using (Tiff tiff = Tiff.ClientOpen(file, "r", fileStream, new TiffStream())) + { + if (tiff == null) + { + throw new NotSupportedException( + $"Unable to open the TIFF file '{file}'. The file may be corrupted or in an unsupported format."); + } + + try + { + frames = ReadTiffFrames(tiff); + } + catch (DllNotFoundException e) + { + throw new DllNotFoundException("Please install BitMiracle.LibTiff.NET from NuGet.", e); + } + } + + if (frames.Count == 0) + { + throw new NotSupportedException( + $"The TIFF file '{file}' was opened but contained no decodable image pages."); + } + + // Hold the decoded pages directly. Binary is deliberately NOT set: the + // source file is larger than a single byte[] can hold, so it is + // re-encoded on demand if the raw bytes are ever requested. + _lazyImage = new Lazy>(() => frames); + ForceLoadLazyImage(); + } + private Lazy> OpenImageToImageSharp(bool preserveOriginalFormat, bool tryWithLibTiff = false) { return new Lazy>(() => From ec97898641ea742a5602d12c6812b023104a107c Mon Sep 17 00:00:00 2001 From: Sawraz Date: Thu, 2 Jul 2026 15:27:56 +0600 Subject: [PATCH 2/2] update(DW-39): fix streamed-TIFF byte/format divergence and >32767-page loop from PR review --- .../UnitTests/AnyBitmapFunctionality.cs | 27 ++++ .../IronSoftware.Drawing.Common/AnyBitmap.cs | 138 ++++++++++++------ 2 files changed, 119 insertions(+), 46 deletions(-) diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs index 7d8225f..9fe5035 100644 --- a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs @@ -600,6 +600,33 @@ public void FromTiffFile_StreamsMultiPageTiff_MatchesFromFile() streamedFrames[i].Width.Should().Be(expectedFrames[i].Width); streamedFrames[i].Height.Should().Be(expectedFrames[i].Height); } + + // Content equivalence, not just dimensions: the decoded pixels of each + // page must match the in-memory loader. + for (int i = 0; i < expectedFrames.Count; i++) + { + expectedFrames[i].SaveAs($"expected-frame{i}.png"); + streamedFrames[i].SaveAs($"streamed-frame{i}.png"); + AssertImageAreEqual($"expected-frame{i}.png", $"streamed-frame{i}.png"); + } + } + + [FactWithAutomaticDisplayName] + public void FromTiffFile_RawBytesAndFormat_ReportTiff() + { + string tiffPath = GetRelativeFilePath("IRON-274-39065.tif"); + + var streamed = AnyBitmap.FromTiffFile(tiffPath); + + // The streamed bitmap keeps no original byte[]; the raw-byte accessors and + // GetImageFormat must re-encode the pages as a multi-page TIFF (matching + // FromFile) rather than reporting a single-page BMP. + streamed.GetImageFormat().Should().Be(AnyBitmap.ImageFormat.Tiff); + + byte[] bytes = streamed.GetBytes(); + var roundTrip = AnyBitmap.FromBytes(bytes); + roundTrip.GetImageFormat().Should().Be(AnyBitmap.ImageFormat.Tiff); + roundTrip.FrameCount.Should().Be(streamed.FrameCount); } [FactWithAutomaticDisplayName] diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs index d652de4..d5e99d7 100644 --- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs @@ -76,6 +76,14 @@ private void ForceLoadLazyImage() private readonly object _binaryLock = new object(); private byte[] _binary; + /// + /// True when this bitmap was produced by streaming a TIFF from disk + /// (), in which case no original byte[] was + /// retained and the raw-byte accessors must re-encode the pages as a multi-page + /// TIFF rather than the default single-frame encoder. + /// + private bool _streamedFromTiff; + /// /// This value save the original bytes, we need to update it each time that Image object (inside _lazyImage) is changed /// @@ -98,9 +106,17 @@ private byte[] Binary { //Which mean we need to update _binary to sync with the image using var stream = new MemoryStream(); - IImageEncoder enc = GetDefaultImageExportEncoder(); - GetFirstInternalImage().Save(stream, enc); + if (_streamedFromTiff) + { + InternalSaveAsMultiPageTiff(_lazyImage?.Value, stream); + } + else + { + IImageEncoder enc = GetDefaultImageExportEncoder(); + GetFirstInternalImage().Save(stream, enc); + } + _binary = stream.ToArray(); IsDirty = false; } @@ -898,6 +914,19 @@ public static AnyBitmap FromFile(string file, bool preserveOriginalFormat) /// automatically falls back to this loader when a TIFF file exceeds the /// in-memory size limit, so calling it explicitly is only required when you /// want to force page-by-page streaming regardless of file size. + /// + /// Two behaviours differ from the in-memory path and are worth noting. + /// First, pages are decoded one at a time but are all retained together, so + /// the resident memory of the returned bitmap scales with page count + /// multiplied by decoded page size. Second, no original byte[] is kept: the + /// raw-byte accessors (, , + /// ) and re-encode the pages + /// as a multi-page TIFF on demand, so they report TIFF (as with + /// ) rather than the original file bytes. For a + /// document whose re-encoded form would exceed ~2 GB, requesting the raw bytes + /// throws the .NET single-array limit; use to + /// persist all pages to disk instead. + /// /// /// public static AnyBitmap FromTiffFile(string file) @@ -2904,11 +2933,14 @@ private IReadOnlyList InternalLoadTiff() /// /// /// The supplied may be backed by any stream - an - /// in-memory buffer or a . When it is backed by a - /// file, LibTiff seeks to each directory on demand, so the whole file is - /// never buffered. This is what allows multi-page TIFF files larger than - /// 2 GB to be loaded: total file size is unbounded, only an individual - /// page must fit within a single decode buffer. + /// in-memory buffer or a . Decoding is performed one + /// page at a time, and when the source is a file LibTiff seeks to each + /// directory on demand, so the whole file is never read into a single buffer. + /// This is what allows multi-page TIFF files larger than 2 GB to be loaded: + /// the total file size is unbounded and only an individual page must fit + /// within a single decode buffer. Note that the decoded pages are all + /// retained together in the returned list, so the resident memory of the + /// result still scales with page count multiplied by decoded page size. /// private List ReadTiffFrames(Tiff tiff) { @@ -2920,56 +2952,61 @@ private List ReadTiffFrames(Tiff tiff) SetTiffCompression(tiff); List images = new(); - short num = tiff.NumberOfDirectories(); - for (short i = 0; i < num; i++) - { - _ = tiff.SetDirectory(i); - if (IsThumbnail(tiff)) + int index = 0; + do + { + if (!IsThumbnail(tiff)) { - continue; - } + var (width, height, horizontalResolution, verticalResolution) = SetWidthHeight(tiff, index, ref imageWidth, ref imageHeight, ref imageXResolution, ref imageYResolution); - var (width, height, horizontalResolution, verticalResolution) = SetWidthHeight(tiff, i, ref imageWidth, ref imageHeight, ref imageXResolution, ref imageYResolution); + // A single page is still decoded into one RGBA buffer, so its pixel + // count is bounded by the .NET single-array index limit. Multi-page + // files of any total size are fine as long as each page fits. + long pixelCount = (long)width * height; + if (pixelCount > MaxSingleFrameRasterPixels) + { + throw new NotSupportedException( + $"TIFF page {index} is {width}x{height} ({pixelCount:N0} pixels), which exceeds the maximum of " + + $"{MaxSingleFrameRasterPixels:N0} pixels that can be decoded into a single buffer. " + + "Split this page into smaller images before loading."); + } - // A single page is still decoded into one RGBA buffer, so its pixel - // count is bounded by the .NET single-array index limit. Multi-page - // files of any total size are fine as long as each page fits. - long pixelCount = (long)width * height; - if (pixelCount > MaxSingleFrameRasterPixels) - { - throw new NotSupportedException( - $"TIFF page {i} is {width}x{height} ({pixelCount:N0} pixels), which exceeds the maximum of " + - $"{MaxSingleFrameRasterPixels:N0} pixels that can be decoded into a single buffer. " + - "Split this page into smaller images before loading."); - } + // Read the image into the memory buffer + int[] raster = new int[height * width]; + if (!tiff.ReadRGBAImage(width, height, raster)) + { + throw new NotSupportedException("Could not read image"); + } - // Read the image into the memory buffer - int[] raster = new int[height * width]; - if (!tiff.ReadRGBAImage(width, height, raster)) - { - throw new NotSupportedException("Could not read image"); - } + var bits = PrepareByteArray(raster, width, height, 32); - var bits = PrepareByteArray(raster, width, height, 32); + var image = Image.LoadPixelData(bits, width, height); - var image = Image.LoadPixelData(bits, width, height); + image.Metadata.HorizontalResolution = horizontalResolution; + image.Metadata.VerticalResolution = verticalResolution; + images.Add(image); - image.Metadata.HorizontalResolution = horizontalResolution; - image.Metadata.VerticalResolution = verticalResolution; - images.Add(image); + //Note1: it might be some case that the bytes of current Image is smaller/bigger than the original tiff + //Note2: 'yield return' make it super slow + } - //Note1: it might be some case that the bytes of current Image is smaller/bigger than the original tiff - //Note2: 'yield return' make it super slow + index++; } + while (tiff.ReadDirectory()); return images; } - // .NET indexes arrays with a 32-bit integer, so a single byte[] (and - // therefore File.ReadAllBytes) cannot exceed ~2 GB. Files above this - // threshold are routed to a streaming loader for TIFF, or rejected with a - // clear message for formats that have no page-based streaming decoder. + // .NET indexes arrays with a 32-bit integer, so a single byte[] (and therefore + // File.ReadAllBytes) cannot exceed Array.MaxLength (~2,147,483,591 bytes). Files + // above the threshold below are routed to a streaming loader for TIFF, or + // rejected with a clear message for formats that have no page-based streaming + // decoder. The threshold is deliberately set a little below Array.MaxLength as a + // safety margin: allocating a byte[] right at the limit is fragile (large object + // heap pressure and borderline OutOfMemoryException). The narrow band between + // this value and Array.MaxLength means a non-TIFF file in that range is now + // rejected rather than loaded; that is an intentional, conservative trade-off. private const long MaxInMemoryFileBytes = 2_000_000_000L; // An Rgba32 page is decoded into one byte[] of width*height*4 bytes; a @@ -2983,6 +3020,12 @@ private List ReadTiffFrames(Tiff tiff) /// are streamed page-by-page from disk; other oversized formats raise a /// clear, actionable exception instead of the opaque .NET array-size error. /// + /// + /// only applies to the normal + /// in-memory route. On the streaming route (a TIFF above the in-memory size + /// limit) pages are always decoded to Rgba32 by LibTiff, so the flag has no + /// effect there. + /// private void LoadImageFromFile(string file, bool preserveOriginalFormat) { long length; @@ -3077,8 +3120,11 @@ private void LoadLargeTiffFromFile(string file) } // Hold the decoded pages directly. Binary is deliberately NOT set: the - // source file is larger than a single byte[] can hold, so it is - // re-encoded on demand if the raw bytes are ever requested. + // source file may be larger than a single byte[] can hold. The + // _streamedFromTiff flag tells the Binary getter to re-encode the pages as + // a multi-page TIFF on demand (so GetImageFormat/GetBytes report TIFF like + // FromFile), rather than the default single-frame encoder. + _streamedFromTiff = true; _lazyImage = new Lazy>(() => frames); ForceLoadLazyImage(); } @@ -3228,7 +3274,7 @@ private ReadOnlySpan PrepareByteArray(int[] raster, int width, int height, return bits; } - private (int width, int height, double horizontalResolution, double verticalResolution) SetWidthHeight(Tiff tiff, short index, ref int imageWidth, ref int imageHeight, ref double imageXResolution, ref double imageYResolution) + private (int width, int height, double horizontalResolution, double verticalResolution) SetWidthHeight(Tiff tiff, int index, ref int imageWidth, ref int imageHeight, ref double imageXResolution, ref double imageYResolution) { // Find the width and height of the image FieldValue[] value = tiff.GetField(TiffTag.IMAGEWIDTH);