diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index aecbbbbc71..b62b0f67b8 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -427,68 +427,49 @@ private void ReadFrame(BufferedReadStream stream, ref Image? ima { this.ReadImageDescriptor(stream); - Buffer2D? indices = null; - try - { - // Determine the color table for this frame. If there is a local one, use it otherwise use the global color table. - bool hasLocalColorTable = this.imageDescriptor.LocalColorTableFlag; - - if (hasLocalColorTable) - { - // Read and store the local color table. We allocate the maximum possible size and slice to match. - int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3; - this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate(768, AllocationOptions.Clean); - stream.Read(this.currentLocalColorTable.GetSpan()[..length]); - } - - indices = this.configuration.MemoryAllocator.Allocate2D(this.imageDescriptor.Width, this.imageDescriptor.Height, AllocationOptions.Clean); - this.ReadFrameIndices(stream, indices); + // Determine the color table for this frame. If there is a local one, use it otherwise use the global color table. + bool hasLocalColorTable = this.imageDescriptor.LocalColorTableFlag; - Span rawColorTable = default; - if (hasLocalColorTable) - { - rawColorTable = this.currentLocalColorTable!.GetSpan()[..this.currentLocalColorTableSize]; - } - else if (this.globalColorTable != null) - { - rawColorTable = this.globalColorTable.GetSpan(); - } - - ReadOnlySpan colorTable = MemoryMarshal.Cast(rawColorTable); - this.ReadFrameColors(ref image, ref previousFrame, indices, colorTable, this.imageDescriptor); + if (hasLocalColorTable) + { + // Read and store the local color table. We allocate the maximum possible size and slice to match. + int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3; + this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate(768, AllocationOptions.Clean); + stream.Read(this.currentLocalColorTable.GetSpan()[..length]); + } - // Skip any remaining blocks - SkipBlock(stream); + Span rawColorTable = default; + if (hasLocalColorTable) + { + rawColorTable = this.currentLocalColorTable!.GetSpan()[..this.currentLocalColorTableSize]; } - finally + else if (this.globalColorTable != null) { - indices?.Dispose(); + rawColorTable = this.globalColorTable.GetSpan(); } - } - /// - /// Reads the frame indices marking the color to use for each pixel. - /// - /// The containing image data. - /// The 2D pixel buffer to write to. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ReadFrameIndices(BufferedReadStream stream, Buffer2D indices) - { - int minCodeSize = stream.ReadByte(); - using LzwDecoder lzwDecoder = new(this.configuration.MemoryAllocator, stream); - lzwDecoder.DecodePixels(minCodeSize, indices); + ReadOnlySpan colorTable = MemoryMarshal.Cast(rawColorTable); + this.ReadFrameColors(stream, ref image, ref previousFrame, colorTable, this.imageDescriptor); + + // Skip any remaining blocks + SkipBlock(stream); } /// /// Reads the frames colors, mapping indices to colors. /// /// The pixel format. + /// The containing image data. /// The image to decode the information to. /// The previous frame. - /// The indexed pixels. /// The color table containing the available colors. /// The - private void ReadFrameColors(ref Image? image, ref ImageFrame? previousFrame, Buffer2D indices, ReadOnlySpan colorTable, in GifImageDescriptor descriptor) + private void ReadFrameColors( + BufferedReadStream stream, + ref Image? image, + ref ImageFrame? previousFrame, + ReadOnlySpan colorTable, + in GifImageDescriptor descriptor) where TPixel : unmanaged, IPixel { int imageWidth = this.logicalScreenDescriptor.Width; @@ -549,73 +530,83 @@ private void ReadFrameColors(ref Image? image, ref ImageFrame indicesRowOwner = this.memoryAllocator.Allocate(descriptor.Width); + Span indicesRow = indicesRowOwner.Memory.Span; + ref byte indicesRowRef = ref MemoryMarshal.GetReference(indicesRow); + + int minCodeSize = stream.ReadByte(); + if (LzwDecoder.IsValidMinCodeSize(minCodeSize)) { - ref byte indicesRowRef = ref MemoryMarshal.GetReference(indices.DangerousGetRowSpan(y - descriptorTop)); + using LzwDecoder lzwDecoder = new(this.configuration.MemoryAllocator, stream, minCodeSize); - // Check if this image is interlaced. - int writeY; // the target y offset to write to - if (descriptor.InterlaceFlag) + for (int y = descriptorTop; y < descriptorBottom && y < imageHeight; y++) { - // If so then we read lines at predetermined offsets. - // When an entire image height worth of offset lines has been read we consider this a pass. - // With each pass the number of offset lines changes and the starting line changes. - if (interlaceY >= descriptor.Height) + // Check if this image is interlaced. + int writeY; // the target y offset to write to + if (descriptor.InterlaceFlag) { - interlacePass++; - switch (interlacePass) + // If so then we read lines at predetermined offsets. + // When an entire image height worth of offset lines has been read we consider this a pass. + // With each pass the number of offset lines changes and the starting line changes. + if (interlaceY >= descriptor.Height) { - case 1: - interlaceY = 4; - break; - case 2: - interlaceY = 2; - interlaceIncrement = 4; - break; - case 3: - interlaceY = 1; - interlaceIncrement = 2; - break; + interlacePass++; + switch (interlacePass) + { + case 1: + interlaceY = 4; + break; + case 2: + interlaceY = 2; + interlaceIncrement = 4; + break; + case 3: + interlaceY = 1; + interlaceIncrement = 2; + break; + } } - } - writeY = interlaceY + descriptor.Top; - interlaceY += interlaceIncrement; - } - else - { - writeY = y; - } + writeY = Math.Min(interlaceY + descriptor.Top, image.Height); + interlaceY += interlaceIncrement; + } + else + { + writeY = y; + } - ref TPixel rowRef = ref MemoryMarshal.GetReference(imageFrame.PixelBuffer.DangerousGetRowSpan(writeY)); + lzwDecoder.DecodePixelRow(indicesRow); + ref TPixel rowRef = ref MemoryMarshal.GetReference(imageFrame.PixelBuffer.DangerousGetRowSpan(writeY)); - if (!transFlag) - { - // #403 The left + width value can be larger than the image width - for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++) + if (!transFlag) { - int index = Numerics.Clamp(Unsafe.Add(ref indicesRowRef, (uint)(x - descriptorLeft)), 0, colorTableMaxIdx); - ref TPixel pixel = ref Unsafe.Add(ref rowRef, (uint)x); - Rgb24 rgb = colorTable[index]; - pixel.FromRgb24(rgb); + // #403 The left + width value can be larger than the image width + for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++) + { + int index = Numerics.Clamp(Unsafe.Add(ref indicesRowRef, (uint)(x - descriptorLeft)), 0, colorTableMaxIdx); + ref TPixel pixel = ref Unsafe.Add(ref rowRef, (uint)x); + Rgb24 rgb = colorTable[index]; + pixel.FromRgb24(rgb); + } } - } - else - { - for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++) + else { - int rawIndex = Unsafe.Add(ref indicesRowRef, (uint)(x - descriptorLeft)); - - // Treat any out of bounds values as transparent. - if (rawIndex > colorTableMaxIdx || rawIndex == transIndex) + for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++) { - continue; - } + int index = Unsafe.Add(ref indicesRowRef, (uint)(x - descriptorLeft)); + + // Treat any out of bounds values as transparent. + if (index > colorTableMaxIdx || index == transIndex) + { + continue; + } - int index = Numerics.Clamp(rawIndex, 0, colorTableMaxIdx); - ref TPixel pixel = ref Unsafe.Add(ref rowRef, (uint)x); - Rgb24 rgb = colorTable[index]; - pixel.FromRgb24(rgb); + ref TPixel pixel = ref Unsafe.Add(ref rowRef, (uint)x); + Rgb24 rgb = colorTable[index]; + pixel.FromRgb24(rgb); + } } } } @@ -656,8 +647,11 @@ private void ReadFrameMetadata(BufferedReadStream stream, List private readonly IMemoryOwner suffix; + /// + /// The scratch buffer for reading data blocks. + /// + private readonly IMemoryOwner scratchBuffer; + /// /// The pixel stack buffer. /// private readonly IMemoryOwner pixelStack; + private readonly int minCodeSize; + private readonly int clearCode; + private readonly int endCode; + private int code; + private int codeSize; + private int codeMask; + private int availableCode; + private int oldCode = NullCode; + private int bits; + private int top; + private int count; + private int bufferIndex; + private int data; + private int first; /// /// Initializes a new instance of the class @@ -55,335 +74,277 @@ internal sealed class LzwDecoder : IDisposable /// /// The to use for buffer allocations. /// The stream to read from. + /// The minimum code size. /// is null. - public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream) + public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream, int minCodeSize) { this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); this.prefix = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean); this.suffix = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean); this.pixelStack = memoryAllocator.Allocate(MaxStackSize + 1, AllocationOptions.Clean); + this.scratchBuffer = memoryAllocator.Allocate(byte.MaxValue, AllocationOptions.None); + this.minCodeSize = minCodeSize; + + // Calculate the clear code. The value of the clear code is 2 ^ minCodeSize + this.clearCode = 1 << minCodeSize; + this.codeSize = minCodeSize + 1; + this.codeMask = (1 << this.codeSize) - 1; + this.endCode = this.clearCode + 1; + this.availableCode = this.clearCode + 2; + + ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan()); + for (this.code = 0; this.code < this.clearCode; this.code++) + { + Unsafe.Add(ref suffixRef, (uint)this.code) = (byte)this.code; + } } /// - /// Decodes and decompresses all pixel indices from the stream, assigning the pixel values to the buffer. + /// Gets a value indicating whether the minimum code size is valid. /// - /// Minimum code size of the data. - /// The pixel array to decode to. - public void DecodePixels(int minCodeSize, Buffer2D pixels) + /// The minimum code size. + /// + /// if the minimum code size is valid; otherwise, . + /// + public static bool IsValidMinCodeSize(int minCodeSize) { - // Calculate the clear code. The value of the clear code is 2 ^ minCodeSize - int clearCode = 1 << minCodeSize; - // It is possible to specify a larger LZW minimum code size than the palette length in bits // which may leave a gap in the codes where no colors are assigned. // http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp#lzw_compression + int clearCode = 1 << minCodeSize; if (minCodeSize < 2 || minCodeSize > MaximumLzwBits || clearCode > MaxStackSize) { // Don't attempt to decode the frame indices. // Theoretically we could determine a min code size from the length of the provided // color palette but we won't bother since the image is most likely corrupted. - return; + return false; } - // The resulting index table length. - int width = pixels.Width; - int height = pixels.Height; - int length = width * height; - - int codeSize = minCodeSize + 1; - - // Calculate the end code - int endCode = clearCode + 1; - - // Calculate the available code. - int availableCode = clearCode + 2; - - // Jillzhangs Code see: http://giflib.codeplex.com/ - // Adapted from John Cristy's ImageMagick. - int code; - int oldCode = NullCode; - int codeMask = (1 << codeSize) - 1; - int bits = 0; - - int top = 0; - int count = 0; - int bi = 0; - int xyz = 0; + return true; + } - int data = 0; - int first = 0; + /// + /// Decodes and decompresses all pixel indices for a single row from the stream, assigning the pixel values to the buffer. + /// + /// The pixel indices array to decode to. + public void DecodePixelRow(Span indices) + { + indices.Clear(); + ref byte pixelsRowRef = ref MemoryMarshal.GetReference(indices); ref int prefixRef = ref MemoryMarshal.GetReference(this.prefix.GetSpan()); ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan()); ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan()); + Span buffer = this.scratchBuffer.GetSpan(); - for (code = 0; code < clearCode; code++) - { - Unsafe.Add(ref suffixRef, (uint)code) = (byte)code; - } - - Span buffer = stackalloc byte[byte.MaxValue]; - - int y = 0; int x = 0; - int rowMax = width; - ref byte pixelsRowRef = ref MemoryMarshal.GetReference(pixels.DangerousGetRowSpan(y)); - while (xyz < length) + int xyz = 0; + while (xyz < indices.Length) { - // Reset row reference. - if (xyz == rowMax) - { - x = 0; - pixelsRowRef = ref MemoryMarshal.GetReference(pixels.DangerousGetRowSpan(++y)); - rowMax = (y * width) + width; - } - - if (top == 0) + if (this.top == 0) { - if (bits < codeSize) + if (this.bits < this.codeSize) { // Load bytes until there are enough bits for a code. - if (count == 0) + if (this.count == 0) { // Read a new data block. - count = this.ReadBlock(buffer); - if (count == 0) + this.count = this.ReadBlock(buffer); + if (this.count == 0) { break; } - bi = 0; + this.bufferIndex = 0; } - data += buffer[bi] << bits; + this.data += buffer[this.bufferIndex] << this.bits; - bits += 8; - bi++; - count--; + this.bits += 8; + this.bufferIndex++; + this.count--; continue; } // Get the next code - code = data & codeMask; - data >>= codeSize; - bits -= codeSize; + this.code = this.data & this.codeMask; + this.data >>= this.codeSize; + this.bits -= this.codeSize; // Interpret the code - if (code > availableCode || code == endCode) + if (this.code > this.availableCode || this.code == this.endCode) { break; } - if (code == clearCode) + if (this.code == this.clearCode) { // Reset the decoder - codeSize = minCodeSize + 1; - codeMask = (1 << codeSize) - 1; - availableCode = clearCode + 2; - oldCode = NullCode; + this.codeSize = this.minCodeSize + 1; + this.codeMask = (1 << this.codeSize) - 1; + this.availableCode = this.clearCode + 2; + this.oldCode = NullCode; continue; } - if (oldCode == NullCode) + if (this.oldCode == NullCode) { - Unsafe.Add(ref pixelStackRef, (uint)top++) = Unsafe.Add(ref suffixRef, (uint)code); - oldCode = code; - first = code; + Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code); + this.oldCode = this.code; + this.first = this.code; continue; } - int inCode = code; - if (code == availableCode) + int inCode = this.code; + if (this.code == this.availableCode) { - Unsafe.Add(ref pixelStackRef, (uint)top++) = (byte)first; + Unsafe.Add(ref pixelStackRef, (uint)this.top++) = (byte)this.first; - code = oldCode; + this.code = this.oldCode; } - while (code > clearCode) + while (this.code > this.clearCode) { - Unsafe.Add(ref pixelStackRef, (uint)top++) = Unsafe.Add(ref suffixRef, (uint)code); - code = Unsafe.Add(ref prefixRef, (uint)code); + Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code); + this.code = Unsafe.Add(ref prefixRef, (uint)this.code); } - int suffixCode = Unsafe.Add(ref suffixRef, (uint)code); - first = suffixCode; - Unsafe.Add(ref pixelStackRef, (uint)top++) = suffixCode; + int suffixCode = Unsafe.Add(ref suffixRef, (uint)this.code); + this.first = suffixCode; + Unsafe.Add(ref pixelStackRef, (uint)this.top++) = suffixCode; // Fix for Gifs that have "deferred clear code" as per here : // https://bugzilla.mozilla.org/show_bug.cgi?id=55918 - if (availableCode < MaxStackSize) + if (this.availableCode < MaxStackSize) { - Unsafe.Add(ref prefixRef, (uint)availableCode) = oldCode; - Unsafe.Add(ref suffixRef, (uint)availableCode) = first; - availableCode++; - if (availableCode == codeMask + 1 && availableCode < MaxStackSize) + Unsafe.Add(ref prefixRef, (uint)this.availableCode) = this.oldCode; + Unsafe.Add(ref suffixRef, (uint)this.availableCode) = this.first; + this.availableCode++; + if (this.availableCode == this.codeMask + 1 && this.availableCode < MaxStackSize) { - codeSize++; - codeMask = (1 << codeSize) - 1; + this.codeSize++; + this.codeMask = (1 << this.codeSize) - 1; } } - oldCode = inCode; + this.oldCode = inCode; } // Pop a pixel off the pixel stack. - top--; + this.top--; // Clear missing pixels xyz++; - Unsafe.Add(ref pixelsRowRef, (uint)x++) = (byte)Unsafe.Add(ref pixelStackRef, (uint)top); + Unsafe.Add(ref pixelsRowRef, (uint)x++) = (byte)Unsafe.Add(ref pixelStackRef, (uint)this.top); } } /// /// Decodes and decompresses all pixel indices from the stream allowing skipping of the data. /// - /// Minimum code size of the data. /// The resulting index table length. - public void SkipIndices(int minCodeSize, int length) + public void SkipIndices(int length) { - // Calculate the clear code. The value of the clear code is 2 ^ minCodeSize - int clearCode = 1 << minCodeSize; - - // It is possible to specify a larger LZW minimum code size than the palette length in bits - // which may leave a gap in the codes where no colors are assigned. - // http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp#lzw_compression - if (minCodeSize < 2 || minCodeSize > MaximumLzwBits || clearCode > MaxStackSize) - { - // Don't attempt to decode the frame indices. - // Theoretically we could determine a min code size from the length of the provided - // color palette but we won't bother since the image is most likely corrupted. - return; - } - - int codeSize = minCodeSize + 1; - - // Calculate the end code - int endCode = clearCode + 1; - - // Calculate the available code. - int availableCode = clearCode + 2; - - // Jillzhangs Code see: http://giflib.codeplex.com/ - // Adapted from John Cristy's ImageMagick. - int code; - int oldCode = NullCode; - int codeMask = (1 << codeSize) - 1; - int bits = 0; - - int top = 0; - int count = 0; - int bi = 0; - int xyz = 0; - - int data = 0; - int first = 0; - ref int prefixRef = ref MemoryMarshal.GetReference(this.prefix.GetSpan()); ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan()); ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan()); + Span buffer = this.scratchBuffer.GetSpan(); - for (code = 0; code < clearCode; code++) - { - Unsafe.Add(ref suffixRef, (uint)code) = (byte)code; - } - - Span buffer = stackalloc byte[byte.MaxValue]; + int xyz = 0; while (xyz < length) { - if (top == 0) + if (this.top == 0) { - if (bits < codeSize) + if (this.bits < this.codeSize) { // Load bytes until there are enough bits for a code. - if (count == 0) + if (this.count == 0) { // Read a new data block. - count = this.ReadBlock(buffer); - if (count == 0) + this.count = this.ReadBlock(buffer); + if (this.count == 0) { break; } - bi = 0; + this.bufferIndex = 0; } - data += buffer[bi] << bits; + this.data += buffer[this.bufferIndex] << this.bits; - bits += 8; - bi++; - count--; + this.bits += 8; + this.bufferIndex++; + this.count--; continue; } // Get the next code - code = data & codeMask; - data >>= codeSize; - bits -= codeSize; + this.code = this.data & this.codeMask; + this.data >>= this.codeSize; + this.bits -= this.codeSize; // Interpret the code - if (code > availableCode || code == endCode) + if (this.code > this.availableCode || this.code == this.endCode) { break; } - if (code == clearCode) + if (this.code == this.clearCode) { // Reset the decoder - codeSize = minCodeSize + 1; - codeMask = (1 << codeSize) - 1; - availableCode = clearCode + 2; - oldCode = NullCode; + this.codeSize = this.minCodeSize + 1; + this.codeMask = (1 << this.codeSize) - 1; + this.availableCode = this.clearCode + 2; + this.oldCode = NullCode; continue; } - if (oldCode == NullCode) + if (this.oldCode == NullCode) { - Unsafe.Add(ref pixelStackRef, (uint)top++) = Unsafe.Add(ref suffixRef, (uint)code); - oldCode = code; - first = code; + Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code); + this.oldCode = this.code; + this.first = this.code; continue; } - int inCode = code; - if (code == availableCode) + int inCode = this.code; + if (this.code == this.availableCode) { - Unsafe.Add(ref pixelStackRef, (uint)top++) = (byte)first; + Unsafe.Add(ref pixelStackRef, (uint)this.top++) = (byte)this.first; - code = oldCode; + this.code = this.oldCode; } - while (code > clearCode) + while (this.code > this.clearCode) { - Unsafe.Add(ref pixelStackRef, (uint)top++) = Unsafe.Add(ref suffixRef, (uint)code); - code = Unsafe.Add(ref prefixRef, (uint)code); + Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code); + this.code = Unsafe.Add(ref prefixRef, (uint)this.code); } - int suffixCode = Unsafe.Add(ref suffixRef, (uint)code); - first = suffixCode; - Unsafe.Add(ref pixelStackRef, (uint)top++) = suffixCode; + int suffixCode = Unsafe.Add(ref suffixRef, (uint)this.code); + this.first = suffixCode; + Unsafe.Add(ref pixelStackRef, (uint)this.top++) = suffixCode; // Fix for Gifs that have "deferred clear code" as per here : // https://bugzilla.mozilla.org/show_bug.cgi?id=55918 - if (availableCode < MaxStackSize) + if (this.availableCode < MaxStackSize) { - Unsafe.Add(ref prefixRef, (uint)availableCode) = oldCode; - Unsafe.Add(ref suffixRef, (uint)availableCode) = first; - availableCode++; - if (availableCode == codeMask + 1 && availableCode < MaxStackSize) + Unsafe.Add(ref prefixRef, (uint)this.availableCode) = this.oldCode; + Unsafe.Add(ref suffixRef, (uint)this.availableCode) = this.first; + this.availableCode++; + if (this.availableCode == this.codeMask + 1 && this.availableCode < MaxStackSize) { - codeSize++; - codeMask = (1 << codeSize) - 1; + this.codeSize++; + this.codeMask = (1 << this.codeSize) - 1; } } - oldCode = inCode; + this.oldCode = inCode; } // Pop a pixel off the pixel stack. - top--; + this.top--; // Clear missing pixels xyz++; @@ -419,5 +380,6 @@ public void Dispose() this.prefix.Dispose(); this.suffix.Dispose(); this.pixelStack.Dispose(); + this.scratchBuffer.Dispose(); } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index 3d53d5bfe8..7dca1cf5ea 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -146,7 +146,7 @@ public static int EstimateQuality(ref Block8x8F table, ReadOnlySpan target quality = (int)Math.Round(5000.0 / sumPercent); } - return quality; + return Numerics.Clamp(quality, MinQualityFactor, MaxQualityFactor); } /// diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs index ee108b9d96..f4e6487a57 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs @@ -204,6 +204,17 @@ public void Issue1530_BadDescriptorDimensions(TestImageProvider image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); } + // https://github.com/SixLabors/ImageSharp/issues/2758 + [Theory] + [WithFile(TestImages.Gif.Issues.Issue2758, PixelTypes.Rgba32)] + public void Issue2758_BadDescriptorDimensions(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + image.DebugSaveMultiFrame(provider); + image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); + } + // https://github.com/SixLabors/ImageSharp/issues/405 [Theory] [WithFile(TestImages.Gif.Issues.BadAppExtLength, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs index 1c203e7342..e68dd1f879 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Tests.TestUtilities; // ReSharper disable InconsistentNaming @@ -425,6 +426,32 @@ public void EncodedStringTags_Read() VerifyEncodedStrings(exif); } + // https://github.com/SixLabors/ImageSharp/issues/2758 + [Theory] + [WithFile(TestImages.Jpeg.Issues.Issue2758, PixelTypes.L8)] + public void Issue2758_DecodeWorks(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(JpegDecoder.Instance); + + Assert.Equal(59787, image.Width); + Assert.Equal(511, image.Height); + + JpegMetadata meta = image.Metadata.GetJpegMetadata(); + + // Quality determination should be between 1-100. + Assert.Equal(15, meta.LuminanceQuality); + Assert.Equal(1, meta.ChrominanceQuality); + + // We want to test the encoder to ensure the determined values can be encoded but not by encoding + // the full size image as it would be too slow. + // We will crop the image to a smaller size and then encode it. + image.Mutate(x => x.Crop(new(0, 0, 100, 100))); + + using MemoryStream ms = new(); + image.Save(ms, new JpegEncoder()); + } + private static void VerifyEncodedStrings(ExifProfile exif) { Assert.NotNull(exif); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index c528251b3e..c571619d64 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -320,6 +320,7 @@ public static class Issues public const string HangBadScan = "Jpg/issues/Hang_C438A851.jpg"; public const string Issue2517 = "Jpg/issues/issue2517-bad-d7.jpg"; public const string Issue2638 = "Jpg/issues/Issue2638.jpg"; + public const string Issue2758 = "Jpg/issues/issue-2758.jpg"; public static class Fuzz { @@ -530,6 +531,7 @@ public static class Issues public const string Issue2450_A = "Gif/issues/issue_2450.gif"; public const string Issue2450_B = "Gif/issues/issue_2450_2.gif"; public const string Issue2198 = "Gif/issues/issue_2198.gif"; + public const string Issue2758 = "Gif/issues/issue_2758.gif"; } public static readonly string[] Animated = diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012BadMinCode_Rgba32_issue2012_drona1.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012BadMinCode_Rgba32_issue2012_drona1.png index cdba9277b1..b07e806620 100644 --- a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012BadMinCode_Rgba32_issue2012_drona1.png +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012BadMinCode_Rgba32_issue2012_drona1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3a24c066895fd3a76649da376485cbc1912d6a3ae15369575f523e66364b3b6 -size 141563 +oid sha256:588d055a93c7b4fdb62e8b77f3ae08753a9e8990151cb0523f5e761996189b70 +size 142244 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/00.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/00.png new file mode 100644 index 0000000000..f63cc98ada --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f39b23217f1d095eeb8eed5ccea36be813c307a60ef4b1942e9f74028451c38 +size 81944 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/01.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/01.png new file mode 100644 index 0000000000..f63cc98ada --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f39b23217f1d095eeb8eed5ccea36be813c307a60ef4b1942e9f74028451c38 +size 81944 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/07.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/07.png index 75cf685e43..efba40c99d 100644 --- a/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/07.png +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/07.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:489642f0c81fd12e97007fe6feb11b0e93e351199a922ce038069a3782ad0722 -size 135 +oid sha256:5016a323018f09e292165ad5392d82dcbad5e79c2b6b93aff3322dffff80b309 +size 126 diff --git a/tests/Images/Input/Gif/issues/issue_2758.gif b/tests/Images/Input/Gif/issues/issue_2758.gif new file mode 100644 index 0000000000..17db9fa132 --- /dev/null +++ b/tests/Images/Input/Gif/issues/issue_2758.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13e9374181c7536d1d2ecb514753a5290c0ec06234ca079c6c8c8a832586b668 +size 199 diff --git a/tests/Images/Input/Jpg/issues/issue-2758.jpg b/tests/Images/Input/Jpg/issues/issue-2758.jpg new file mode 100644 index 0000000000..48ee1159ec --- /dev/null +++ b/tests/Images/Input/Jpg/issues/issue-2758.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f32a238b57b7073f7442f8ae7efd6ba3ae4cda30d57e6666fb8a1eaa27108558 +size 1412