diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs index 03cd639ad3..86b5c19d21 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs @@ -28,20 +28,20 @@ protected TiffBaseDecompressor(MemoryAllocator memoryAllocator, int width, int b /// Decompresses image data into the supplied buffer. /// /// The to read image data from. - /// The strip offset of stream. - /// The number of bytes to read from the input stream. + /// The data offset within the stream. + /// The number of bytes to read from the input stream. /// The height of the strip. /// The output buffer for uncompressed data. /// The token to monitor cancellation. - public void Decompress(BufferedReadStream stream, ulong stripOffset, ulong stripByteCount, int stripHeight, Span buffer, CancellationToken cancellationToken) + public void Decompress(BufferedReadStream stream, ulong offset, ulong count, int stripHeight, Span buffer, CancellationToken cancellationToken) { - DebugGuard.MustBeLessThanOrEqualTo(stripOffset, (ulong)long.MaxValue, nameof(stripOffset)); - DebugGuard.MustBeLessThanOrEqualTo(stripByteCount, (ulong)long.MaxValue, nameof(stripByteCount)); + DebugGuard.MustBeLessThanOrEqualTo(offset, (ulong)long.MaxValue, nameof(offset)); + DebugGuard.MustBeLessThanOrEqualTo(count, (ulong)int.MaxValue, nameof(count)); - stream.Seek((long)stripOffset, SeekOrigin.Begin); - this.Decompress(stream, (int)stripByteCount, stripHeight, buffer, cancellationToken); + stream.Seek((long)offset, SeekOrigin.Begin); + this.Decompress(stream, (int)count, stripHeight, buffer, cancellationToken); - if ((long)stripOffset + (long)stripByteCount < stream.Position) + if ((long)offset + (long)count < stream.Position) { TiffThrowHelper.ThrowImageFormatException("Out of range when reading a strip."); } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index e66f0f3765..134f8e2aa0 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Tiff.Compression; +using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; using SixLabors.ImageSharp.IO; @@ -403,8 +404,14 @@ private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStr { for (int stripIndex = 0; stripIndex < stripBuffers.Length; stripIndex++) { - int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip, stripIndex); - stripBuffers[stripIndex] = this.memoryAllocator.Allocate(uncompressedStripSize); + ulong uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip, stripIndex); + + if (uncompressedStripSize > int.MaxValue) + { + TiffThrowHelper.ThrowNotSupported("Strips larger than Int32.MaxValue bytes are not supported for compressed images."); + } + + stripBuffers[stripIndex] = this.memoryAllocator.Allocate((int)uncompressedStripSize); } using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); @@ -460,26 +467,97 @@ private void DecodeStripsChunky(ImageFrame frame, int rowsPerStr rowsPerStrip = frame.Height; } - int uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip); + ulong uncompressedStripSize = this.CalculateStripBufferSize(frame.Width, rowsPerStrip); int bitsPerPixel = this.BitsPerPixel; - - using IMemoryOwner stripBuffer = this.memoryAllocator.Allocate(uncompressedStripSize, AllocationOptions.Clean); - Span stripBufferSpan = stripBuffer.GetSpan(); Buffer2D pixels = frame.PixelBuffer; - using TiffBaseDecompressor decompressor = this.CreateDecompressor(frame.Width, bitsPerPixel); + int width = frame.Width; + int height = frame.Height; + + using TiffBaseDecompressor decompressor = this.CreateDecompressor(width, bitsPerPixel); TiffBaseColorDecoder colorDecoder = this.CreateChunkyColorDecoder(); + // 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) + { + ulong bytesPerRowU = this.CalculateStripBufferSize(frame.Width, 1); + + // This should never happen, but we check just to be sure. + if (bytesPerRowU > int.MaxValue) + { + TiffThrowHelper.ThrowNotSupported("Strips larger than Int32.MaxValue bytes are not supported for compressed images."); + } + + int bytesPerRow = (int)bytesPerRowU; + using IMemoryOwner rowBufferOwner = this.memoryAllocator.Allocate(bytesPerRow, AllocationOptions.Clean); + Span rowBuffer = rowBufferOwner.GetSpan(); + for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) + { + cancellationToken.ThrowIfCancellationRequested(); + + int stripHeight = stripIndex < stripOffsets.Length - 1 || height % rowsPerStrip == 0 + ? rowsPerStrip + : height % rowsPerStrip; + + int top = rowsPerStrip * stripIndex; + if (top + stripHeight > height) + { + break; + } + + 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); + + // Use the NoneTiffCompression decompressor to read exactly one row. + none.Decompress( + this.inputStream, + rowOffset, + (ulong)bytesPerRow, + 1, + rowBuffer, + cancellationToken); + + colorDecoder.Decode(rowBuffer, pixels, 0, top + r, width, 1); + } + } + + return; + } + + if (uncompressedStripSize > int.MaxValue) + { + TiffThrowHelper.ThrowNotSupported("Strips larger than Int32.MaxValue bytes are not supported for compressed images."); + } + + using IMemoryOwner stripBuffer = this.memoryAllocator.Allocate((int)uncompressedStripSize, AllocationOptions.Clean); + Span stripBufferSpan = stripBuffer.GetSpan(); + for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) { cancellationToken.ThrowIfCancellationRequested(); - int stripHeight = stripIndex < stripOffsets.Length - 1 || frame.Height % rowsPerStrip == 0 + int stripHeight = stripIndex < stripOffsets.Length - 1 || height % rowsPerStrip == 0 ? rowsPerStrip - : frame.Height % rowsPerStrip; + : height % rowsPerStrip; int top = rowsPerStrip * stripIndex; - if (top + stripHeight > frame.Height) + if (top + stripHeight > height) { // Make sure we ignore any strips that are not needed for the image (if too many are present). break; @@ -493,7 +571,7 @@ private void DecodeStripsChunky(ImageFrame frame, int rowsPerStr stripBufferSpan, cancellationToken); - colorDecoder.Decode(stripBufferSpan, pixels, 0, top, frame.Width, stripHeight); + colorDecoder.Decode(stripBufferSpan, pixels, 0, top, width, stripHeight); } } @@ -753,7 +831,7 @@ private IMemoryOwner ConvertNumbers(Array array, out Span span) /// The height for the desired pixel buffer. /// The index of the plane for planar image configuration (or zero for chunky). /// The size (in bytes) of the required pixel buffer. - private int CalculateStripBufferSize(int width, int height, int plane = -1) + private ulong CalculateStripBufferSize(int width, int height, int plane = -1) { DebugGuard.MustBeLessThanOrEqualTo(plane, 3, nameof(plane)); @@ -786,8 +864,8 @@ private int CalculateStripBufferSize(int width, int height, int plane = -1) } } - int bytesPerRow = ((width * bitsPerPixel) + 7) / 8; - return bytesPerRow * height; + ulong bytesPerRow = (((ulong)width * (ulong)bitsPerPixel) + 7) / 8; + return bytesPerRow * (ulong)height; } ///