diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs index dc9e1e7296..74914e036f 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs @@ -126,6 +126,14 @@ internal sealed class T4BitCompressor : TiffCcittCompressor } } + /// + protected override long GetMaximumEncodedBits(int rowsPerStrip) + { + // A pixel can require a 13-bit terminating code. Each row can also require + // an 8-bit zero-length white run and a 12-bit EOL, plus the initial EOL. + return 12L + ((((long)this.Width * 13) + 20) * rowsPerStrip); + } + private void WriteEndOfLine(Span compressedData) { if (this.useModifiedHuffman) diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs index cffc96fcdf..6dba8b7d25 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs @@ -131,6 +131,14 @@ internal sealed class T6BitCompressor : TiffCcittCompressor this.WriteCode(12, 1, compressedData); } + /// + protected override long GetMaximumEncodedBits(int rowsPerStrip) + { + // Alternating pixels use at most 29 bits per two-pixel horizontal mode. + // Allow 16 bits per pixel, row transition overhead, and the final 24-bit EOFB. + return ((((long)this.Width * 16) + 24) * rowsPerStrip) + 24; + } + /// protected override void Dispose(bool disposing) { diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs index 8e2227cba5..06cf84437a 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs @@ -465,6 +465,12 @@ internal abstract class TiffCcittCompressor : TiffBaseCompressor /// The destination buffer to write the code to. protected void WriteCode(uint codeLength, uint code, Span compressedData) { + long availableBits = (((long)compressedData.Length - this.bytePosition) * 8) - this.bitPosition; + if (codeLength > availableBits) + { + throw new InvalidMemoryOperationException("The CCITT output buffer is too small for the encoded data."); + } + while (codeLength > 0) { int bitNumber = (int)codeLength; @@ -526,8 +532,20 @@ internal abstract class TiffCcittCompressor : TiffBaseCompressor /// public override void Initialize(int rowsPerStrip) { - // This is too much memory allocated, but just 1 bit per pixel will not do, if the compression rate is not good. - int maxNeededBytes = this.Width * rowsPerStrip; - this.compressedDataBuffer = this.Allocator.Allocate(maxNeededBytes); + long maxNeededBits = this.GetMaximumEncodedBits(rowsPerStrip); + ulong maxNeededBytes = (ulong)((maxNeededBits + 7) / 8); + if (maxNeededBytes > int.MaxValue) + { + InvalidMemoryOperationException.ThrowAllocationOverLimitException(maxNeededBytes, int.MaxValue); + } + + this.compressedDataBuffer = this.Allocator.Allocate((int)maxNeededBytes); } + + /// + /// Gets an upper bound for the encoded strip length in bits. + /// + /// The number of rows in the strip. + /// The maximum encoded length. + protected abstract long GetMaximumEncodedBits(int rowsPerStrip); } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index 4317c2714d..5e663b8f3c 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -554,6 +554,27 @@ public class TiffEncoderTests : TiffEncoderBaseTester public void TiffEncoder_EncodeBiColor_WithCcittGroup3FaxCompression_BlackIsZero_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup3Fax); + [Fact] + public void TiffEncoder_EncodeNarrowCcittGroup3Fax_Works() + { + using Image image = new(1, 2000); + for (int y = 0; y < image.Height; y++) + { + image[0, y] = new L8((byte)((y & 1) == 0 ? 255 : 0)); + } + + TiffFrameMetadata metadata = image.Frames.RootFrame.Metadata.GetTiffMetadata(); + metadata.BitsPerPixel = TiffBitsPerPixel.Bit1; + metadata.Compression = TiffCompression.CcittGroup3Fax; + + using MemoryStream output = new(); + image.Save(output, new TiffEncoder()); + + output.Position = 0; + using Image decoded = Image.Load(output); + Assert.Equal(image.Size, decoded.Size); + } + [Theory] [WithFile(Issues2255, PixelTypes.Rgba32)] public void TiffEncoder_EncodeBiColor_WithCcittGroup3FaxCompression_WithoutSpecifyingBitPerPixel_Works(TestImageProvider provider)