diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/NoneTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/NoneTiffCompression.cs
index 0c601e9ddb..335c0ae635 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/NoneTiffCompression.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/NoneTiffCompression.cs
@@ -11,20 +11,37 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors;
///
internal sealed class NoneTiffCompression : TiffBaseDecompressor
{
+ private readonly SegmentIntegrityHandling integrityHandling;
+
///
/// Initializes a new instance of the class.
///
/// The memory allocator.
/// The width of the image.
/// The bits per pixel.
- public NoneTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel)
+ /// The policy for incomplete image data.
+ public NoneTiffCompression(MemoryAllocator memoryAllocator, int width, int bitsPerPixel, SegmentIntegrityHandling integrityHandling)
: base(memoryAllocator, width, bitsPerPixel)
{
+ this.integrityHandling = integrityHandling;
}
///
protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer, CancellationToken cancellationToken)
- => _ = stream.Read(buffer, 0, Math.Min(buffer.Length, byteCount));
+ {
+ // The caller supplies only the rows needed for this read. Respect both the declared byte count and end of file.
+ int bytesRead = stream.Read(buffer, 0, Math.Min(buffer.Length, byteCount));
+ if (bytesRead < buffer.Length)
+ {
+ if (this.integrityHandling != SegmentIntegrityHandling.IgnoreImageData)
+ {
+ throw new InvalidImageContentException("TIFF strip or tile contains incomplete image data.");
+ }
+
+ // The buffer is reused. Missing samples must not retain bytes from a previous strip, row, or pooled allocation.
+ buffer[bytesRead..].Clear();
+ }
+ }
///
protected override void Dispose(bool disposing)
diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs
index 1eaa64c43b..bb3d7e18b6 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs
@@ -40,7 +40,7 @@ public static TiffBaseDecompressor Create(
case TiffDecoderCompressionType.None:
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
DebugGuard.IsTrue(faxOptions == FaxCompressionOptions.None, "No fax compression options are expected");
- return new NoneTiffCompression(allocator, width, bitsPerPixel);
+ return new NoneTiffCompression(allocator, width, bitsPerPixel, options.SegmentIntegrityHandling);
case TiffDecoderCompressionType.PackBits:
DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
diff --git a/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs b/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs
index 18c0f2e0d3..fb9d6d5d7b 100644
--- a/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs
+++ b/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs
@@ -74,8 +74,17 @@ private static ByteOrder ReadByteOrder(Stream stream)
private List ReadIfds(bool isBigTiff)
{
List readers = [];
+
+ // TIFF allows directories at any offset, so a chain that goes backwards in the file is valid.
+ // Only a directory offset that has already been read means the chain loops.
+ HashSet visitedOffsets = new(4);
while (this.nextIfdOffset != 0 && this.nextIfdOffset < (ulong)this.stream.Length)
{
+ if (!visitedOffsets.Add(this.nextIfdOffset))
+ {
+ TiffThrowHelper.ThrowImageFormatException("TIFF image contains circular directory offsets");
+ }
+
EntryReader reader = new(this.stream, this.ByteOrder, this.allocator);
reader.ReadTags(isBigTiff, this.nextIfdOffset);
@@ -90,11 +99,6 @@ private List ReadIfds(bool isBigTiff)
}
}
- if (this.nextIfdOffset >= reader.NextIfdOffset && reader.NextIfdOffset != 0)
- {
- TiffThrowHelper.ThrowImageFormatException("TIFF image contains circular directory offsets");
- }
-
this.nextIfdOffset = reader.NextIfdOffset;
readers.Add(reader);
diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
index 413e970836..dbd844faec 100644
--- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
+++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
@@ -184,7 +184,6 @@ protected override Image Decode(BufferedReadStream stream, Cance
}
}
- this.Dimensions = frames[0].Size;
ImageMetadata metadata = TiffDecoderMetadataCreator.Create(framesMetadata, this.skipMetadata, reader.ByteOrder, reader.IsBigTiff);
return new Image(this.configuration, metadata, frames);
}
@@ -253,30 +252,38 @@ private ImageFrame DecodeFrame(ExifProfile tags, Size? size, Can
}
else
{
+ // Record the root size before allocation so allocation errors report the image dimensions.
size = new Size(width, height);
+ this.Dimensions = size.Value;
}
ImageFrame frame = new(this.configuration, size.Value.Width, size.Value.Height, metadata.FrameMetadata);
- if (isTiled)
- {
- this.DecodeImageWithTiles(tags, frame, width, height, cancellationToken);
- }
- else
+ try
{
- this.DecodeImageWithStrips(tags, frame, width, height, cancellationToken);
- }
+ if (isTiled)
+ {
+ this.DecodeImageWithTiles(tags, frame, width, height, cancellationToken);
+ }
+ else
+ {
+ this.DecodeImageWithStrips(tags, frame, width, height, cancellationToken);
+ }
+
+ // This shared conversion accepts RGB-compatible pixels. Other TIFF color models convert while decoding their samples.
+ if (this.ColorType is >= TiffColorType.PaletteColor and <= TiffColorType.Rgba32323232Planar)
+ {
+ _ = this.TryConvertIccProfile(frame);
+ }
- // Only RGB-compatible color types can be converted here because the TPixel-based ICC profile conversion
- // expects RGB-like pixel data; other photometric interpretations (YCbCr, CMYK, Lab, etc.) would require
- // dedicated transforms. We do this once at the frame level to avoid duplicating conversion logic
- // across all color decoders and to keep their decode paths focused on raw pixel unpacking.
- if (this.ColorType is >= TiffColorType.PaletteColor and <= TiffColorType.Rgba32323232Planar)
+ return frame;
+ }
+ catch
{
- _ = this.TryConvertIccProfile(frame);
+ // The caller takes ownership only after this method returns, including any ICC conversion.
+ frame.Dispose();
+ throw;
}
-
- return frame;
}
private (ImageFrameMetadata FrameMetadata, TiffFrameMetadata TiffMetadata) CreateFrameMetadata(ExifProfile tags)
@@ -323,14 +330,18 @@ private ImageFrame DecodeFrame(ExifProfile tags, Size? size, Can
private void DecodeImageWithStrips(ExifProfile tags, ImageFrame frame, int width, int height, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
- int rowsPerStrip;
+ // TIFF 6.0 defaults RowsPerStrip to 2^32 - 1 (page 39). A value at least as large as height describes one strip.
+ // Both layouts allocate storage for the rows that exist, not the larger declared strip height.
+ int rowsPerStrip = height;
if (tags.TryGetValue(ExifTag.RowsPerStrip, out IExifValue value))
{
- rowsPerStrip = (int)value.Value;
+ rowsPerStrip = Math.Min((int)value.Value, height);
}
- else
+
+ // TIFF 6.0 requires at least one row per strip. Zero cannot define row offsets or a strip count.
+ if (rowsPerStrip <= 0)
{
- rowsPerStrip = TiffConstants.RowsPerStripInfinity;
+ TiffThrowHelper.ThrowImageFormatException("RowsPerStrip must be greater than zero.");
}
Array stripOffsetsArray = (Array)tags.GetValueInternal(ExifTag.StripOffsets).GetValue();
@@ -451,13 +462,31 @@ private void DecodeStripsPlanar(
Buffer2D pixels = frame.PixelBuffer;
+ using TiffBaseDecompressor decompressor = this.CreateDecompressor(width, bitsPerPixel, frame.Metadata);
+
+ // Read an uncompressed plane in rows when its strip exceeds the single-buffer limit.
+ // Each row has a fixed byte count, so its file offset does not depend on decoding earlier rows.
+ bool readRows = false;
+ if (decompressor is NoneTiffCompression && this.CanReadStripRows())
+ {
+ for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++)
+ {
+ if (this.CalculateStripBufferSize(width, rowsPerStrip, planeIndex) > (ulong)this.memoryAllocator.SingleBufferAllocationLimitBytes)
+ {
+ readRows = true;
+ break;
+ }
+ }
+ }
+
+ int rowsPerRead = readRows ? 1 : rowsPerStrip;
IMemoryOwner[] stripBuffers = new IMemoryOwner[stripsPerPixel];
try
{
for (int stripIndex = 0; stripIndex < stripBuffers.Length; stripIndex++)
{
- ulong uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip, stripIndex);
+ ulong uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerRead, stripIndex);
if (uncompressedStripSize > int.MaxValue)
{
@@ -467,7 +496,6 @@ private void DecodeStripsPlanar(
stripBuffers[stripIndex] = this.memoryAllocator.Allocate((int)uncompressedStripSize);
}
- using TiffBaseDecompressor decompressor = this.CreateDecompressor(width, bitsPerPixel, frame.Metadata);
TiffBasePlanarColorDecoder colorDecoder = this.CreatePlanarColorDecoder(frame.Metadata);
for (int i = 0; i < stripsPerPlane; i++)
@@ -476,21 +504,46 @@ private void DecodeStripsPlanar(
int stripHeight = i < stripsPerPlane - 1 || height % rowsPerStrip == 0 ? rowsPerStrip : height % rowsPerStrip;
- int stripIndex = i;
- for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++)
+ for (int row = 0; row < stripHeight; row += rowsPerRead)
{
- decompressor.Decompress(
- this.inputStream,
- stripOffsets[stripIndex],
- stripByteCounts[stripIndex],
- stripHeight,
- stripBuffers[planeIndex].GetSpan(),
- cancellationToken);
+ cancellationToken.ThrowIfCancellationRequested();
- stripIndex += stripsPerPlane;
- }
+ int readHeight = Math.Min(rowsPerRead, stripHeight - row);
+ int stripIndex = i;
+ for (int planeIndex = 0; planeIndex < stripsPerPixel; planeIndex++)
+ {
+ ulong offset = stripOffsets[stripIndex];
+ ulong byteCount = stripByteCounts[stripIndex];
+ if (readRows)
+ {
+ // Read the one row at its position in the strip of this plane. A strip that is shorter
+ // than its rows is read only up to its end, as when the strip is read whole.
+ ulong bytesPerRow = this.CalculateStripBufferSize(width, 1, planeIndex);
+ ulong rowStart = (ulong)row * bytesPerRow;
+ offset += rowStart;
+ byteCount = byteCount > rowStart ? Math.Min(byteCount - rowStart, bytesPerRow) : 0;
+ }
+
+ Span stripBuffer = stripBuffers[planeIndex].GetSpan();
+ if (decompressor is NoneTiffCompression)
+ {
+ // The last strip can be shorter than the reusable buffer. Padding is not required image data.
+ stripBuffer = stripBuffer[..(int)this.CalculateUncompressedDataSize(width, readHeight, planeIndex)];
+ }
+
+ decompressor.Decompress(
+ this.inputStream,
+ offset,
+ byteCount,
+ readHeight,
+ stripBuffer,
+ cancellationToken);
+
+ stripIndex += stripsPerPlane;
+ }
- colorDecoder.Decode(stripBuffers, pixels, 0, rowsPerStrip * i, width, stripHeight);
+ colorDecoder.Decode(stripBuffers, pixels, 0, (rowsPerStrip * i) + row, width, readHeight);
+ }
}
}
finally
@@ -523,12 +576,6 @@ private void DecodeStripsChunky(
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
- // If the rowsPerStrip has the default value, which is effectively infinity. That is, the entire image is one strip.
- if (rowsPerStrip == TiffConstants.RowsPerStripInfinity)
- {
- rowsPerStrip = height;
- }
-
ulong uncompressedStripSize = this.CalculateStripBufferSize(width, rowsPerStrip);
int bitsPerPixel = this.BitsPerPixel;
@@ -536,13 +583,11 @@ private void DecodeStripsChunky(
TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder(frame.Metadata);
Buffer2D pixels = frame.PixelBuffer;
- // There exists in this world TIFF files with uncompressed strips larger than Int32.MaxValue.
- // We can read them, but we cannot allocate a buffer that large to hold the uncompressed data.
- // In this scenario we fall back to reading and decoding one row at a time.
- //
- // The NoneTiffCompression decompressor can be used to read individual rows since we have
- // a guarantee that each row required the same number of bytes.
- if (decompressor is NoneTiffCompression none && uncompressedStripSize > int.MaxValue)
+ // An uncompressed strip can exceed the single-buffer limit even when the decoded image fits in memory.
+ // Read it in rows. Each row has a fixed byte count and uses the same reusable buffer.
+ if (decompressor is NoneTiffCompression none
+ && uncompressedStripSize > (ulong)this.memoryAllocator.SingleBufferAllocationLimitBytes
+ && this.CanReadStripRows())
{
ulong bytesPerRowU = this.CalculateStripBufferSize(width, 1);
@@ -571,23 +616,19 @@ private void DecodeStripsChunky(
ulong baseOffset = stripOffsets[stripIndex];
ulong available = stripByteCounts[stripIndex];
- ulong required = (ulong)bytesPerRow * (ulong)stripHeight;
- if (available < required)
- {
- break;
- }
-
for (int r = 0; r < stripHeight; r++)
{
cancellationToken.ThrowIfCancellationRequested();
- ulong rowOffset = baseOffset + ((ulong)r * (ulong)bytesPerRow);
+ ulong rowStart = (ulong)r * (ulong)bytesPerRow;
+ ulong rowOffset = baseOffset + rowStart;
+ ulong rowByteCount = available > rowStart ? Math.Min(available - rowStart, (ulong)bytesPerRow) : 0;
- // Use the NoneTiffCompression decompressor to read exactly one row.
+ // Keep reads within this strip. The decompressor applies the integrity policy to missing row samples.
none.Decompress(
this.inputStream,
rowOffset,
- (ulong)bytesPerRow,
+ rowByteCount,
1,
rowBuffer,
cancellationToken);
@@ -631,12 +672,19 @@ private void DecodeStripsChunky(
break;
}
+ Span stripData = stripBufferSpan;
+ if (decompressor is NoneTiffCompression)
+ {
+ // Only the final strip's actual rows must be present. The buffer can also hold a full strip.
+ stripData = stripData[..(int)this.CalculateUncompressedDataSize(width, stripHeight, -1)];
+ }
+
decompressor.Decompress(
this.inputStream,
stripOffsets[stripIndex],
stripByteCounts[stripIndex],
stripHeight,
- stripBufferSpan,
+ stripData,
cancellationToken);
colorDecoder.Decode(stripBufferSpan, pixels, 0, top, width, stripHeight);
@@ -686,11 +734,16 @@ private void DecodeTilesPlanar(
try
{
- int bytesPerTileRow = RoundUpToMultipleOfEight(tileWidth * bitsPerPixel);
- int uncompressedTilesSize = bytesPerTileRow * tileLength;
for (int i = 0; i < tilesBuffers.Length; i++)
{
- tilesBuffers[i] = this.memoryAllocator.Allocate(uncompressedTilesSize, AllocationOptions.Clean);
+ // Each buffer holds one plane, not every component of a pixel. Tile padding still uses the full tile dimensions.
+ ulong uncompressedTileSize = this.CalculateStripBufferSize(tileWidth, tileLength, i);
+ if (uncompressedTileSize > int.MaxValue)
+ {
+ TiffThrowHelper.ThrowNotSupported("Tiles larger than Int32.MaxValue bytes are not supported.");
+ }
+
+ tilesBuffers[i] = this.memoryAllocator.Allocate((int)uncompressedTileSize, AllocationOptions.Clean);
}
using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel, frame.Metadata, true, tileWidth, tileLength);
@@ -712,12 +765,18 @@ private void DecodeTilesPlanar(
{
cancellationToken.ThrowIfCancellationRequested();
+ Span tileBuffer = tilesBuffers[i].GetSpan();
+ if (decompressor is NoneTiffCompression)
+ {
+ tileBuffer = tileBuffer[..(int)this.CalculateUncompressedDataSize(tileWidth, tileLength, i)];
+ }
+
decompressor.Decompress(
this.inputStream,
tileOffsets[tileIndexForChannel],
tileByteCounts[tileIndexForChannel],
tileLength,
- tilesBuffers[i].GetSpan(),
+ tileBuffer,
cancellationToken);
tileIndexForChannel += tilesPerChannel;
@@ -938,6 +997,12 @@ private ulong CalculateStripBufferSize(int width, int height, int plane = -1)
if (this.PlanarConfiguration == TiffPlanarConfiguration.Chunky)
{
DebugGuard.IsTrue(plane == -1, "Expected Chunky planar.");
+
+ if (this.CompressionType == TiffDecoderCompressionType.None && !this.CanReadStripRows())
+ {
+ return this.CalculateUncompressedDataSize(width, height, plane);
+ }
+
bitsPerPixel = this.BitsPerPixel;
}
else
@@ -966,6 +1031,45 @@ private ulong CalculateStripBufferSize(int width, int height, int plane = -1)
return bytesPerRow * (ulong)height;
}
+ ///
+ /// Calculates the encoded byte count for an uncompressed strip, tile, or individual row.
+ ///
+ /// The stored row width, including tile padding when applicable.
+ /// The number of stored rows.
+ /// The planar component index, or -1 for chunky data.
+ /// The number of encoded bytes required for these samples.
+ private ulong CalculateUncompressedDataSize(int width, int height, int plane)
+ {
+ if (this.CanReadStripRows())
+ {
+ return this.CalculateStripBufferSize(width, height, plane);
+ }
+
+ ulong horizontal = this.YcbcrSubSampling[0];
+ ulong vertical = this.YcbcrSubSampling[1];
+ ulong groupsAcross = ((ulong)width + horizontal - 1) / horizontal;
+ ulong groupsDown = ((ulong)height + vertical - 1) / vertical;
+
+ // Chunky YCbCr stores horizontal * vertical luminance samples, then one Cb and one Cr sample per group.
+ // Planar YCbCr stores full-resolution luminance and one sample per group in each separate chroma plane.
+ // Chroma expansion needs larger plane buffers, but those extra bytes are not required in the file.
+ return this.PlanarConfiguration == TiffPlanarConfiguration.Chunky
+ ? groupsAcross * groupsDown * ((horizontal * vertical) + 2)
+ : plane == 0 ? (ulong)width * (ulong)height : groupsAcross * groupsDown;
+ }
+
+ ///
+ /// Determines whether each uncompressed row can be read and decoded separately.
+ ///
+ /// if the rows of a strip can be decoded one at a time.
+ private bool CanReadStripRows()
+ {
+ // Subsampled YCbCr groups can span rows. Read the complete strip before expanding its chroma samples.
+ return this.PhotometricInterpretation != TiffPhotometricInterpretation.YCbCr
+ || this.YcbcrSubSampling is null
+ || (this.YcbcrSubSampling[0] == 1 && this.YcbcrSubSampling[1] == 1);
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int RoundUpToMultipleOfEight(int value) => (int)(((uint)value + 7) / 8);
}
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs
index b911d1b177..e059ff3c5d 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/NoneTiffCompressionTests.cs
@@ -18,7 +18,7 @@ public void Decompress_ReadsData(byte[] inputData, uint byteCount, byte[] expect
using BufferedReadStream stream = new(Configuration.Default, memoryStream);
byte[] buffer = new byte[expectedResult.Length];
- using NoneTiffCompression decompressor = new(default, default, default);
+ using NoneTiffCompression decompressor = new(default, default, default, default);
decompressor.Decompress(stream, 0, byteCount, 1, buffer, default);
Assert.Equal(expectedResult, buffer);
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderStripLayoutTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderStripLayoutTests.cs
new file mode 100644
index 0000000000..9a5bb40a8d
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderStripLayoutTests.cs
@@ -0,0 +1,241 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
+using static SixLabors.ImageSharp.Tests.TestImages.Tiff;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Tiff;
+
+///
+/// Tests strip allocation and directory traversal with stored TIFF inputs.
+///
+[Trait("Format", "Tiff")]
+[ValidateDisposedMemoryAllocations]
+public class TiffDecoderStripLayoutTests
+{
+ ///
+ /// A missing RowsPerStrip or a value greater than the image height describes one strip per plane.
+ ///
+ /// The stored TIFF input.
+ [Theory]
+ [InlineData(ChunkyRowsPerStripMissing)]
+ [InlineData(ChunkyRowsPerStripLarge)]
+ [InlineData(ChunkyRowsPerStripMax)]
+ [InlineData(PlanarRowsPerStripMissing)]
+ [InlineData(PlanarRowsPerStripLarge)]
+ [InlineData(PlanarRowsPerStripMax)]
+ public void Decode_RowsPerStripMissingOrAboveHeight_DecodesAllRows(string path)
+ {
+ using Image expected = Image.Load(TestFile.Create(StripLayoutRgbReference).Bytes);
+ byte[] data = TestFile.Create(path).Bytes;
+
+ foreach (SegmentIntegrityHandling integrity in Enum.GetValues())
+ {
+ DecoderOptions options = new() { SegmentIntegrityHandling = integrity };
+ using Image actual = Image.Load(options, data);
+
+ ImageComparer.Exact.VerifySimilarity(expected, actual);
+ }
+ }
+
+ ///
+ /// Reading an uncompressed strip in rows preserves every 16-bit component.
+ ///
+ /// The stored single-strip or multiple-strip input.
+ [Theory]
+ [InlineData(ChunkyRgb16SingleStrip)]
+ [InlineData(ChunkyRgb16MultipleStrips)]
+ [InlineData(PlanarRgb16SingleStrip)]
+ [InlineData(PlanarRgb16MultipleStrips)]
+ public void Decode_UncompressedStripAboveBufferLimit_DecodesAllRows(string path)
+ {
+ byte[] data = TestFile.Create(path).Bytes;
+ using Image expected = Image.Load(data);
+ Configuration configuration = Configuration.Default.Clone();
+ configuration.MemoryAllocator = MemoryAllocator.Create(new MemoryAllocatorOptions { SingleBufferAllocationLimitMegabytes = 1 });
+
+ // Each full strip exceeds 1 MiB, including each separate plane. A row fits within the limit.
+ // The final strip in the multiple-strip inputs contains only the remaining 100 rows.
+ foreach (SegmentIntegrityHandling integrity in Enum.GetValues())
+ {
+ DecoderOptions options = new() { Configuration = configuration, SegmentIntegrityHandling = integrity };
+ using Image actual = Image.Load(options, data);
+
+ ImageComparer.Exact.VerifySimilarity(expected, actual);
+ }
+ }
+
+ ///
+ /// Subsampled chroma planes require fewer stored samples than their expanded destination buffers.
+ ///
+ [Fact]
+ public void Decode_SubsampledPlanarYCbCr_DoesNotRequireExpandedChromaInFile()
+ {
+ byte[] data = TestFile.Create(PlanarYCbCrSubsampled).Bytes;
+
+ foreach (SegmentIntegrityHandling integrity in Enum.GetValues())
+ {
+ DecoderOptions options = new() { SegmentIntegrityHandling = integrity };
+ using Image image = Image.Load(options, data);
+
+ // Y = 64 with neutral Cb and Cr = 128 represents gray 64 in every pixel.
+ Assert.Equal(new Size(4, 2), image.Size);
+ for (int y = 0; y < image.Height; y++)
+ {
+ for (int x = 0; x < image.Width; x++)
+ {
+ Assert.Equal(new Rgb24(64, 64, 64), image[x, y]);
+ }
+ }
+ }
+ }
+
+ ///
+ /// A zero strip height is invalid in every integrity mode.
+ ///
+ /// The stored TIFF with RowsPerStrip set to zero.
+ [Theory]
+ [InlineData(ChunkyRowsPerStripZero)]
+ [InlineData(PlanarRowsPerStripZero)]
+ public void Decode_ZeroRowsPerStrip_ThrowsImageFormatException(string path)
+ {
+ byte[] data = TestFile.Create(path).Bytes;
+
+ foreach (SegmentIntegrityHandling integrity in Enum.GetValues())
+ {
+ DecoderOptions options = new() { SegmentIntegrityHandling = integrity };
+ Assert.Throws(() =>
+ {
+ using Image image = Image.Load(options, data);
+ });
+ }
+ }
+
+ ///
+ /// Short image data follows the integrity policy without reading past the declared strip or reusing old samples.
+ ///
+ /// The stored TIFF whose strip ends halfway through its second row.
+ /// Whether to force row-by-row reads.
+ [Theory]
+ [InlineData(ChunkyRgb16ShortStrip, false)]
+ [InlineData(ChunkyRgb16ShortStrip, true)]
+ [InlineData(PlanarRgb16ShortStrip, false)]
+ [InlineData(PlanarRgb16ShortStrip, true)]
+ public void Decode_ShortUncompressedStrip_UsesImageDataIntegrityPolicy(string path, bool limitBuffer)
+ {
+ byte[] data = TestFile.Create(path).Bytes;
+ Configuration configuration = Configuration.Default.Clone();
+ if (limitBuffer)
+ {
+ configuration.MemoryAllocator = MemoryAllocator.Create(new MemoryAllocatorOptions { SingleBufferAllocationLimitMegabytes = 1 });
+ }
+
+ foreach (SegmentIntegrityHandling integrity in Enum.GetValues())
+ {
+ DecoderOptions options = new() { Configuration = configuration, SegmentIntegrityHandling = integrity };
+ if (integrity != SegmentIntegrityHandling.IgnoreImageData)
+ {
+ Assert.Throws(() =>
+ {
+ using Image image = Image.Load(options, data);
+ });
+ }
+ else
+ {
+ using Image image = Image.Load(options, data);
+
+ // The fixtures contain more bytes after StripByteCounts. Those bytes must not become image samples.
+ // Samples before the declared end survive. Missing samples are zero in both read paths.
+ Assert.Equal(new Rgb48(0, 13621, 27242), image[0, 0]);
+ Assert.Equal(new Rgb48(25957, 39578, 53199), image[0, 1]);
+ Assert.Equal(default, image[1023, 1]);
+ Assert.Equal(default, image[0, 2]);
+ Assert.Equal(default, image[1023, 699]);
+ }
+ }
+ }
+
+ ///
+ /// A failed image allocation reports the declared dimensions.
+ ///
+ [Fact]
+ public void Decode_FrameAboveAllocationLimit_ThrowsWithRealDimensions()
+ {
+ byte[] data = TestFile.Create(FrameAboveAllocationLimit).Bytes;
+ Configuration configuration = Configuration.Default.Clone();
+ configuration.MemoryAllocator = MemoryAllocator.Create(new MemoryAllocatorOptions { AllocationLimitMegabytes = 100 });
+ DecoderOptions options = new() { Configuration = configuration };
+
+ InvalidImageContentException ex = Assert.Throws(() =>
+ {
+ using Image image = Image.Load(options, data);
+ });
+
+ Assert.Contains("20000x15000", ex.Message, StringComparison.Ordinal);
+ }
+
+ ///
+ /// Directory links determine frame order even when their file offsets decrease.
+ ///
+ /// The stored TIFF or BigTIFF with reversed directory offsets.
+ [Theory]
+ [InlineData(ReverseDirectories)]
+ [InlineData(BigTiffReverseDirectories)]
+ public void Decode_DirectoriesStoredInReverseFileOrder_DecodesAllFrames(string path)
+ {
+ byte[] data = TestFile.Create(path).Bytes;
+
+ foreach (SegmentIntegrityHandling integrity in Enum.GetValues())
+ {
+ DecoderOptions options = new() { SegmentIntegrityHandling = integrity };
+ using Image image = Image.Load(options, data);
+ ImageInfo info = Image.Identify(options, data);
+
+ Assert.Equal(2, image.Frames.Count);
+ Assert.Equal(2, info.FrameMetadataCollection.Count);
+ Assert.Equal(new Size(2, 2), image.Size);
+ Assert.Equal(new L8(10), image.Frames[0][0, 0]);
+ Assert.Equal(new L8(11), image.Frames[0][1, 0]);
+ Assert.Equal(new L8(12), image.Frames[0][0, 1]);
+ Assert.Equal(new L8(13), image.Frames[0][1, 1]);
+ Assert.Equal(new L8(20), image.Frames[1][0, 0]);
+ Assert.Equal(new L8(21), image.Frames[1][1, 0]);
+ Assert.Equal(new L8(22), image.Frames[1][0, 1]);
+ Assert.Equal(new L8(23), image.Frames[1][1, 1]);
+ }
+ }
+
+ ///
+ /// A repeated directory offset is a structural error in every integrity mode.
+ ///
+ /// The stored TIFF or BigTIFF with a circular directory chain.
+ [Theory]
+ [InlineData(DirectoryLoopOne)]
+ [InlineData(DirectoryLoopTwo)]
+ [InlineData(DirectoryLoopTwoReverse)]
+ [InlineData(DirectoryLoopThreeReverse)]
+ [InlineData(BigTiffDirectoryLoopOne)]
+ [InlineData(BigTiffDirectoryLoopTwo)]
+ [InlineData(BigTiffDirectoryLoopTwoReverse)]
+ [InlineData(BigTiffDirectoryLoopThreeReverse)]
+ public void Decode_DirectoryChainRevisitsDirectory_ThrowsImageFormatException(string path)
+ {
+ byte[] data = TestFile.Create(path).Bytes;
+
+ foreach (SegmentIntegrityHandling integrity in Enum.GetValues())
+ {
+ DecoderOptions options = new() { SegmentIntegrityHandling = integrity };
+ ImageFormatException identifyError = Assert.Throws(() => Image.Identify(options, data));
+ ImageFormatException decodeError = Assert.Throws(() =>
+ {
+ using Image image = Image.Load(options, data);
+ });
+
+ Assert.Contains("circular", identifyError.Message, StringComparison.Ordinal);
+ Assert.Contains("circular", decodeError.Message, StringComparison.Ordinal);
+ }
+ }
+}
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index e4c3fffff6..b30226ee87 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -1002,6 +1002,34 @@ public static class Tiff
public const string Benchmark_RgbPackbits = "medium_rgb_packbits.tiff";
public const string Benchmark_RgbUncompressed = "medium_rgb_uncompressed.tiff";
+ public const string StripLayoutRgbReference = "Tiff/strips/rgb-4000x2.png";
+ public const string PlanarYCbCrSubsampled = "Tiff/strips/planar-ycbcr-subsampled.tiff";
+ public const string ChunkyRowsPerStripMissing = "Tiff/strips/chunky-rowsperstrip-missing.tiff";
+ public const string ChunkyRowsPerStripLarge = "Tiff/strips/chunky-rowsperstrip-large.tiff";
+ public const string ChunkyRowsPerStripMax = "Tiff/strips/chunky-rowsperstrip-max.tiff";
+ public const string ChunkyRowsPerStripZero = "Tiff/strips/chunky-rowsperstrip-zero.tiff";
+ public const string PlanarRowsPerStripMissing = "Tiff/strips/planar-rowsperstrip-missing.tiff";
+ public const string PlanarRowsPerStripLarge = "Tiff/strips/planar-rowsperstrip-large.tiff";
+ public const string PlanarRowsPerStripMax = "Tiff/strips/planar-rowsperstrip-max.tiff";
+ public const string PlanarRowsPerStripZero = "Tiff/strips/planar-rowsperstrip-zero.tiff";
+ public const string ChunkyRgb16SingleStrip = "Tiff/strips/chunky-rgb16-700-rows.tiff";
+ public const string ChunkyRgb16MultipleStrips = "Tiff/strips/chunky-rgb16-600-rows.tiff";
+ public const string PlanarRgb16SingleStrip = "Tiff/strips/planar-rgb16-700-rows.tiff";
+ public const string PlanarRgb16MultipleStrips = "Tiff/strips/planar-rgb16-600-rows.tiff";
+ public const string ChunkyRgb16ShortStrip = "Tiff/strips/chunky-rgb16-short-strip.tiff";
+ public const string PlanarRgb16ShortStrip = "Tiff/strips/planar-rgb16-short-strip.tiff";
+ public const string FrameAboveAllocationLimit = "Tiff/strips/frame-above-allocation-limit.tiff";
+ public const string ReverseDirectories = "Tiff/strips/reverse-directories.tiff";
+ public const string BigTiffReverseDirectories = "Tiff/strips/reverse-directories.bigtiff";
+ public const string DirectoryLoopOne = "Tiff/strips/loop-1-forward.tiff";
+ public const string DirectoryLoopTwo = "Tiff/strips/loop-2-forward.tiff";
+ public const string DirectoryLoopTwoReverse = "Tiff/strips/loop-2-reverse.tiff";
+ public const string DirectoryLoopThreeReverse = "Tiff/strips/loop-3-reverse.tiff";
+ public const string BigTiffDirectoryLoopOne = "Tiff/strips/loop-1-forward.bigtiff";
+ public const string BigTiffDirectoryLoopTwo = "Tiff/strips/loop-2-forward.bigtiff";
+ public const string BigTiffDirectoryLoopTwoReverse = "Tiff/strips/loop-2-reverse.bigtiff";
+ public const string BigTiffDirectoryLoopThreeReverse = "Tiff/strips/loop-3-reverse.bigtiff";
+
public const string BilevelWithoutBitsPerSampleUncompressed = "Tiff/defaults/bilevel-no-bitspersample-none.tiff";
public const string BilevelWithoutBitsPerSampleFax4 = "Tiff/defaults/bilevel-no-bitspersample-g4.tiff";
public const string BilevelWithoutSampleTags = "Tiff/defaults/bilevel-no-bitspersample-or-samplesperpixel.tiff";
diff --git a/tests/Images/Input/Tiff/strips/chunky-rgb16-600-rows.tiff b/tests/Images/Input/Tiff/strips/chunky-rgb16-600-rows.tiff
new file mode 100644
index 0000000000..355a61ed5e
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/chunky-rgb16-600-rows.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b47a52f17b1f1b56984f46264c5daa4b023ff85195ef5ceedf61bfaeb86d18c8
+size 4300944
diff --git a/tests/Images/Input/Tiff/strips/chunky-rgb16-700-rows.tiff b/tests/Images/Input/Tiff/strips/chunky-rgb16-700-rows.tiff
new file mode 100644
index 0000000000..83e730d48c
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/chunky-rgb16-700-rows.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:386e773612f8673784d243e251c99657bd5e92204bd8b3fed0c7ec1b3ef16b16
+size 4300928
diff --git a/tests/Images/Input/Tiff/strips/chunky-rgb16-short-strip.tiff b/tests/Images/Input/Tiff/strips/chunky-rgb16-short-strip.tiff
new file mode 100644
index 0000000000..c25ed053c3
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/chunky-rgb16-short-strip.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:240011d8f11a91720343ef6878d9fe1b89018507edcfc48954cd25b89b4bd6b8
+size 4300928
diff --git a/tests/Images/Input/Tiff/strips/chunky-rowsperstrip-large.tiff b/tests/Images/Input/Tiff/strips/chunky-rowsperstrip-large.tiff
new file mode 100644
index 0000000000..725563b927
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/chunky-rowsperstrip-large.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f826e3e26cc5a6b47b7ba186dd05dbe8f3b30faaae055722986426902d8f23db
+size 24128
diff --git a/tests/Images/Input/Tiff/strips/chunky-rowsperstrip-max.tiff b/tests/Images/Input/Tiff/strips/chunky-rowsperstrip-max.tiff
new file mode 100644
index 0000000000..4d9b73ad13
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/chunky-rowsperstrip-max.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:164916f345450cc883b4ed3017ed21f0027543a4ca7d20476fe4e2e27b2e35f6
+size 24128
diff --git a/tests/Images/Input/Tiff/strips/chunky-rowsperstrip-missing.tiff b/tests/Images/Input/Tiff/strips/chunky-rowsperstrip-missing.tiff
new file mode 100644
index 0000000000..1b2e814aa3
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/chunky-rowsperstrip-missing.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:398b7f818dcd20576d7027797ee83871d50b532f31ff3c45339b0e13255a89be
+size 24116
diff --git a/tests/Images/Input/Tiff/strips/chunky-rowsperstrip-zero.tiff b/tests/Images/Input/Tiff/strips/chunky-rowsperstrip-zero.tiff
new file mode 100644
index 0000000000..65e0390f5d
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/chunky-rowsperstrip-zero.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:48267753ca7aa7e4ce87a6c117c2c6fe201a1db7993334bd7e671a10cb116fc2
+size 24128
diff --git a/tests/Images/Input/Tiff/strips/frame-above-allocation-limit.tiff b/tests/Images/Input/Tiff/strips/frame-above-allocation-limit.tiff
new file mode 100644
index 0000000000..97f825e5c2
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/frame-above-allocation-limit.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:129a0ebbb2c88516825de8cd6528f1c1c95a9798eef8f4068c398638ef757bd9
+size 8310
diff --git a/tests/Images/Input/Tiff/strips/loop-1-forward.bigtiff b/tests/Images/Input/Tiff/strips/loop-1-forward.bigtiff
new file mode 100644
index 0000000000..c342c77dc9
Binary files /dev/null and b/tests/Images/Input/Tiff/strips/loop-1-forward.bigtiff differ
diff --git a/tests/Images/Input/Tiff/strips/loop-1-forward.tiff b/tests/Images/Input/Tiff/strips/loop-1-forward.tiff
new file mode 100644
index 0000000000..7ca8ad93fe
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/loop-1-forward.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:274d9263569569b70b329d73f705c5e3ad160ca30331cd7acf90be3f38ed8af7
+size 126
diff --git a/tests/Images/Input/Tiff/strips/loop-2-forward.bigtiff b/tests/Images/Input/Tiff/strips/loop-2-forward.bigtiff
new file mode 100644
index 0000000000..0b64a98a04
Binary files /dev/null and b/tests/Images/Input/Tiff/strips/loop-2-forward.bigtiff differ
diff --git a/tests/Images/Input/Tiff/strips/loop-2-forward.tiff b/tests/Images/Input/Tiff/strips/loop-2-forward.tiff
new file mode 100644
index 0000000000..32593a425e
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/loop-2-forward.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f3ec05f949c46b7394e7a404b9d0c475af63110ce1e8a410dc3dbc3333af1d6c
+size 244
diff --git a/tests/Images/Input/Tiff/strips/loop-2-reverse.bigtiff b/tests/Images/Input/Tiff/strips/loop-2-reverse.bigtiff
new file mode 100644
index 0000000000..8fb7ebc027
Binary files /dev/null and b/tests/Images/Input/Tiff/strips/loop-2-reverse.bigtiff differ
diff --git a/tests/Images/Input/Tiff/strips/loop-2-reverse.tiff b/tests/Images/Input/Tiff/strips/loop-2-reverse.tiff
new file mode 100644
index 0000000000..b265f3d112
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/loop-2-reverse.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2d380758cdc90a0c6c007c3e1de356e517b2ffb3848b238062a5426a8359829f
+size 244
diff --git a/tests/Images/Input/Tiff/strips/loop-3-reverse.bigtiff b/tests/Images/Input/Tiff/strips/loop-3-reverse.bigtiff
new file mode 100644
index 0000000000..74c8a86e2b
Binary files /dev/null and b/tests/Images/Input/Tiff/strips/loop-3-reverse.bigtiff differ
diff --git a/tests/Images/Input/Tiff/strips/loop-3-reverse.tiff b/tests/Images/Input/Tiff/strips/loop-3-reverse.tiff
new file mode 100644
index 0000000000..7a6f06d766
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/loop-3-reverse.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:610beb51d8bdd3ea149ae42bdf0cec72b1d3722f213b42ba6abd3cc680a88a40
+size 362
diff --git a/tests/Images/Input/Tiff/strips/planar-rgb16-600-rows.tiff b/tests/Images/Input/Tiff/strips/planar-rgb16-600-rows.tiff
new file mode 100644
index 0000000000..58a55d3ca5
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/planar-rgb16-600-rows.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:72d37f93a003a09a0321c850b324bf5923acacee445ae24651ccfd8d81081dad
+size 4300988
diff --git a/tests/Images/Input/Tiff/strips/planar-rgb16-700-rows.tiff b/tests/Images/Input/Tiff/strips/planar-rgb16-700-rows.tiff
new file mode 100644
index 0000000000..76a82b5b76
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/planar-rgb16-700-rows.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6fabfd386b261df9bc40de7ff6fe2633b1c2e5d449afd4436496e3f7355fa5b1
+size 4300964
diff --git a/tests/Images/Input/Tiff/strips/planar-rgb16-short-strip.tiff b/tests/Images/Input/Tiff/strips/planar-rgb16-short-strip.tiff
new file mode 100644
index 0000000000..9774488a5c
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/planar-rgb16-short-strip.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8149227403c242ef34feb08b15805665d4c3f14b9faa779570b785b63c8f423a
+size 4300964
diff --git a/tests/Images/Input/Tiff/strips/planar-rowsperstrip-large.tiff b/tests/Images/Input/Tiff/strips/planar-rowsperstrip-large.tiff
new file mode 100644
index 0000000000..425d1d6b42
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/planar-rowsperstrip-large.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f2d0b42daa1edc3bf85ae6a5d3c49d24f5749d7e99f056efd4f82971f8a49b30
+size 24164
diff --git a/tests/Images/Input/Tiff/strips/planar-rowsperstrip-max.tiff b/tests/Images/Input/Tiff/strips/planar-rowsperstrip-max.tiff
new file mode 100644
index 0000000000..33af217696
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/planar-rowsperstrip-max.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b03ad3cfa2506a91f8f12f468d7e91906b2f11522e8b49a117ca3678bf215860
+size 24164
diff --git a/tests/Images/Input/Tiff/strips/planar-rowsperstrip-missing.tiff b/tests/Images/Input/Tiff/strips/planar-rowsperstrip-missing.tiff
new file mode 100644
index 0000000000..2ec0f958cb
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/planar-rowsperstrip-missing.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8ca696692d7a4f0a8e4ef9a74b30017bf94e277c810dd2f2effa0448d1e69cfd
+size 24152
diff --git a/tests/Images/Input/Tiff/strips/planar-rowsperstrip-zero.tiff b/tests/Images/Input/Tiff/strips/planar-rowsperstrip-zero.tiff
new file mode 100644
index 0000000000..0f6987b086
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/planar-rowsperstrip-zero.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:70d0b345720dacfa3f66db0921f6e001c3bef40f19d05f8386a6d710b14a1888
+size 24164
diff --git a/tests/Images/Input/Tiff/strips/planar-ycbcr-subsampled.tiff b/tests/Images/Input/Tiff/strips/planar-ycbcr-subsampled.tiff
new file mode 100644
index 0000000000..d26e8d79c1
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/planar-ycbcr-subsampled.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:929b7c625e1ae3c1b917ae2ce200e621fd9d71d61917fb9898af4094a236064b
+size 188
diff --git a/tests/Images/Input/Tiff/strips/reverse-directories.bigtiff b/tests/Images/Input/Tiff/strips/reverse-directories.bigtiff
new file mode 100644
index 0000000000..f54a4e9c55
Binary files /dev/null and b/tests/Images/Input/Tiff/strips/reverse-directories.bigtiff differ
diff --git a/tests/Images/Input/Tiff/strips/reverse-directories.tiff b/tests/Images/Input/Tiff/strips/reverse-directories.tiff
new file mode 100644
index 0000000000..222f6da2d3
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/reverse-directories.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f9336ef717b1ec742ad861be3c7a9430effc8021af3b9c80e649644d2e239f5e
+size 244
diff --git a/tests/Images/Input/Tiff/strips/rgb-4000x2.png b/tests/Images/Input/Tiff/strips/rgb-4000x2.png
new file mode 100644
index 0000000000..b8ca08efb6
--- /dev/null
+++ b/tests/Images/Input/Tiff/strips/rgb-4000x2.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a8a8e3252c92cc864d24b18f65bddafc9f7eff09c1018f1325dfccd6982baef6
+size 118