Browse Source

Merge branch 'main' into js/colorspace-converter

pull/2739/head
James Jackson-South 2 years ago
committed by GitHub
parent
commit
71d49ae8f9
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 188
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  2. 330
      src/ImageSharp/Formats/Gif/LzwDecoder.cs
  3. 2
      src/ImageSharp/Formats/Jpeg/Components/Quantization.cs
  4. 11
      tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
  5. 28
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs
  6. 2
      tests/ImageSharp.Tests/TestImages.cs
  7. 4
      tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012BadMinCode_Rgba32_issue2012_drona1.png
  8. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/00.png
  9. 3
      tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2758_BadDescriptorDimensions_Rgba32_issue_2758.gif/01.png
  10. 4
      tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/07.png
  11. 3
      tests/Images/Input/Gif/issues/issue_2758.gif
  12. 3
      tests/Images/Input/Jpg/issues/issue-2758.jpg

188
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -427,68 +427,49 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
{ {
this.ReadImageDescriptor(stream); this.ReadImageDescriptor(stream);
Buffer2D<byte>? indices = null; // Determine the color table for this frame. If there is a local one, use it otherwise use the global color table.
try bool hasLocalColorTable = this.imageDescriptor.LocalColorTableFlag;
{
// 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<byte>(768, AllocationOptions.Clean);
stream.Read(this.currentLocalColorTable.GetSpan()[..length]);
}
indices = this.configuration.MemoryAllocator.Allocate2D<byte>(this.imageDescriptor.Width, this.imageDescriptor.Height, AllocationOptions.Clean);
this.ReadFrameIndices(stream, indices);
Span<byte> rawColorTable = default; if (hasLocalColorTable)
if (hasLocalColorTable) {
{ // Read and store the local color table. We allocate the maximum possible size and slice to match.
rawColorTable = this.currentLocalColorTable!.GetSpan()[..this.currentLocalColorTableSize]; int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3;
} this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate<byte>(768, AllocationOptions.Clean);
else if (this.globalColorTable != null) stream.Read(this.currentLocalColorTable.GetSpan()[..length]);
{ }
rawColorTable = this.globalColorTable.GetSpan();
}
ReadOnlySpan<Rgb24> colorTable = MemoryMarshal.Cast<byte, Rgb24>(rawColorTable);
this.ReadFrameColors(ref image, ref previousFrame, indices, colorTable, this.imageDescriptor);
// Skip any remaining blocks Span<byte> rawColorTable = default;
SkipBlock(stream); if (hasLocalColorTable)
{
rawColorTable = this.currentLocalColorTable!.GetSpan()[..this.currentLocalColorTableSize];
} }
finally else if (this.globalColorTable != null)
{ {
indices?.Dispose(); rawColorTable = this.globalColorTable.GetSpan();
} }
}
/// <summary> ReadOnlySpan<Rgb24> colorTable = MemoryMarshal.Cast<byte, Rgb24>(rawColorTable);
/// Reads the frame indices marking the color to use for each pixel. this.ReadFrameColors(stream, ref image, ref previousFrame, colorTable, this.imageDescriptor);
/// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param> // Skip any remaining blocks
/// <param name="indices">The 2D pixel buffer to write to.</param> SkipBlock(stream);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ReadFrameIndices(BufferedReadStream stream, Buffer2D<byte> indices)
{
int minCodeSize = stream.ReadByte();
using LzwDecoder lzwDecoder = new(this.configuration.MemoryAllocator, stream);
lzwDecoder.DecodePixels(minCodeSize, indices);
} }
/// <summary> /// <summary>
/// Reads the frames colors, mapping indices to colors. /// Reads the frames colors, mapping indices to colors.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="image">The image to decode the information to.</param> /// <param name="image">The image to decode the information to.</param>
/// <param name="previousFrame">The previous frame.</param> /// <param name="previousFrame">The previous frame.</param>
/// <param name="indices">The indexed pixels.</param>
/// <param name="colorTable">The color table containing the available colors.</param> /// <param name="colorTable">The color table containing the available colors.</param>
/// <param name="descriptor">The <see cref="GifImageDescriptor"/></param> /// <param name="descriptor">The <see cref="GifImageDescriptor"/></param>
private void ReadFrameColors<TPixel>(ref Image<TPixel>? image, ref ImageFrame<TPixel>? previousFrame, Buffer2D<byte> indices, ReadOnlySpan<Rgb24> colorTable, in GifImageDescriptor descriptor) private void ReadFrameColors<TPixel>(
BufferedReadStream stream,
ref Image<TPixel>? image,
ref ImageFrame<TPixel>? previousFrame,
ReadOnlySpan<Rgb24> colorTable,
in GifImageDescriptor descriptor)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int imageWidth = this.logicalScreenDescriptor.Width; int imageWidth = this.logicalScreenDescriptor.Width;
@ -549,69 +530,79 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
byte transIndex = this.graphicsControlExtension.TransparencyIndex; byte transIndex = this.graphicsControlExtension.TransparencyIndex;
int colorTableMaxIdx = colorTable.Length - 1; int colorTableMaxIdx = colorTable.Length - 1;
for (int y = descriptorTop; y < descriptorBottom && y < imageHeight; y++) // For a properly encoded gif the descriptor dimensions will never exceed the logical screen dimensions.
// However we have images that exceed this that can be decoded by other libraries. #1530
using IMemoryOwner<byte> indicesRowOwner = this.memoryAllocator.Allocate<byte>(descriptor.Width);
Span<byte> 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. for (int y = descriptorTop; y < descriptorBottom && y < imageHeight; y++)
int writeY; // the target y offset to write to
if (descriptor.InterlaceFlag)
{ {
// If so then we read lines at predetermined offsets. // Check if this image is interlaced.
// When an entire image height worth of offset lines has been read we consider this a pass. int writeY; // the target y offset to write to
// With each pass the number of offset lines changes and the starting line changes. if (descriptor.InterlaceFlag)
if (interlaceY >= descriptor.Height)
{ {
interlacePass++; // If so then we read lines at predetermined offsets.
switch (interlacePass) // 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: interlacePass++;
interlaceY = 4; switch (interlacePass)
break; {
case 2: case 1:
interlaceY = 2; interlaceY = 4;
interlaceIncrement = 4; break;
break; case 2:
case 3: interlaceY = 2;
interlaceY = 1; interlaceIncrement = 4;
interlaceIncrement = 2; break;
break; case 3:
interlaceY = 1;
interlaceIncrement = 2;
break;
}
} }
}
writeY = interlaceY + descriptor.Top; writeY = Math.Min(interlaceY + descriptor.Top, image.Height);
interlaceY += interlaceIncrement; interlaceY += interlaceIncrement;
} }
else else
{ {
writeY = y; 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) if (!transFlag)
{
// #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); // #403 The left + width value can be larger than the image width
Unsafe.Add(ref rowRef, (uint)x) = TPixel.FromRgb24(colorTable[index]); for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++)
{
int index = Numerics.Clamp(Unsafe.Add(ref indicesRowRef, (uint)(x - descriptorLeft)), 0, colorTableMaxIdx);
Unsafe.Add(ref rowRef, (uint)x) = TPixel.FromRgb24(colorTable[index]);
}
} }
} else
else
{
for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++)
{ {
int rawIndex = Unsafe.Add(ref indicesRowRef, (uint)(x - descriptorLeft)); for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++)
// Treat any out of bounds values as transparent.
if (rawIndex > colorTableMaxIdx || rawIndex == transIndex)
{ {
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); Unsafe.Add(ref rowRef, (uint)x) = TPixel.FromRgb24(colorTable[index]);
Unsafe.Add(ref rowRef, (uint)x) = TPixel.FromRgb24(colorTable[index]); }
} }
} }
} }
@ -652,8 +643,11 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
// Skip the frame indices. Pixels length + mincode size. // Skip the frame indices. Pixels length + mincode size.
// The gif format does not tell us the length of the compressed data beforehand. // The gif format does not tell us the length of the compressed data beforehand.
int minCodeSize = stream.ReadByte(); int minCodeSize = stream.ReadByte();
using LzwDecoder lzwDecoder = new(this.configuration.MemoryAllocator, stream); if (LzwDecoder.IsValidMinCodeSize(minCodeSize))
lzwDecoder.SkipIndices(minCodeSize, this.imageDescriptor.Width * this.imageDescriptor.Height); {
using LzwDecoder lzwDecoder = new(this.configuration.MemoryAllocator, stream, minCodeSize);
lzwDecoder.SkipIndices(this.imageDescriptor.Width * this.imageDescriptor.Height);
}
ImageFrameMetadata currentFrame = new(); ImageFrameMetadata currentFrame = new();
frameMetadata.Add(currentFrame); frameMetadata.Add(currentFrame);

330
src/ImageSharp/Formats/Gif/LzwDecoder.cs

@ -44,10 +44,29 @@ internal sealed class LzwDecoder : IDisposable
/// </summary> /// </summary>
private readonly IMemoryOwner<int> suffix; private readonly IMemoryOwner<int> suffix;
/// <summary>
/// The scratch buffer for reading data blocks.
/// </summary>
private readonly IMemoryOwner<byte> scratchBuffer;
/// <summary> /// <summary>
/// The pixel stack buffer. /// The pixel stack buffer.
/// </summary> /// </summary>
private readonly IMemoryOwner<int> pixelStack; private readonly IMemoryOwner<int> 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;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="LzwDecoder"/> class /// Initializes a new instance of the <see cref="LzwDecoder"/> class
@ -55,335 +74,277 @@ internal sealed class LzwDecoder : IDisposable
/// </summary> /// </summary>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for buffer allocations.</param> /// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for buffer allocations.</param>
/// <param name="stream">The stream to read from.</param> /// <param name="stream">The stream to read from.</param>
/// <param name="minCodeSize">The minimum code size.</param>
/// <exception cref="ArgumentNullException"><paramref name="stream"/> is null.</exception> /// <exception cref="ArgumentNullException"><paramref name="stream"/> is null.</exception>
public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream) public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream, int minCodeSize)
{ {
this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); this.stream = stream ?? throw new ArgumentNullException(nameof(stream));
this.prefix = memoryAllocator.Allocate<int>(MaxStackSize, AllocationOptions.Clean); this.prefix = memoryAllocator.Allocate<int>(MaxStackSize, AllocationOptions.Clean);
this.suffix = memoryAllocator.Allocate<int>(MaxStackSize, AllocationOptions.Clean); this.suffix = memoryAllocator.Allocate<int>(MaxStackSize, AllocationOptions.Clean);
this.pixelStack = memoryAllocator.Allocate<int>(MaxStackSize + 1, AllocationOptions.Clean); this.pixelStack = memoryAllocator.Allocate<int>(MaxStackSize + 1, AllocationOptions.Clean);
this.scratchBuffer = memoryAllocator.Allocate<byte>(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;
}
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="minCodeSize">Minimum code size of the data.</param> /// <param name="minCodeSize">The minimum code size.</param>
/// <param name="pixels">The pixel array to decode to.</param> /// <returns>
public void DecodePixels(int minCodeSize, Buffer2D<byte> pixels) /// <see langword="true"/> if the minimum code size is valid; otherwise, <see langword="false"/>.
/// </returns>
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 // 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. // 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 // http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp#lzw_compression
int clearCode = 1 << minCodeSize;
if (minCodeSize < 2 || minCodeSize > MaximumLzwBits || clearCode > MaxStackSize) if (minCodeSize < 2 || minCodeSize > MaximumLzwBits || clearCode > MaxStackSize)
{ {
// Don't attempt to decode the frame indices. // Don't attempt to decode the frame indices.
// Theoretically we could determine a min code size from the length of the provided // 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. // color palette but we won't bother since the image is most likely corrupted.
return; return false;
} }
// The resulting index table length. return true;
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;
int data = 0; /// <summary>
int first = 0; /// Decodes and decompresses all pixel indices for a single row from the stream, assigning the pixel values to the buffer.
/// </summary>
/// <param name="indices">The pixel indices array to decode to.</param>
public void DecodePixelRow(Span<byte> indices)
{
indices.Clear();
ref byte pixelsRowRef = ref MemoryMarshal.GetReference(indices);
ref int prefixRef = ref MemoryMarshal.GetReference(this.prefix.GetSpan()); ref int prefixRef = ref MemoryMarshal.GetReference(this.prefix.GetSpan());
ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan()); ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan());
ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan()); ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan());
Span<byte> buffer = this.scratchBuffer.GetSpan();
for (code = 0; code < clearCode; code++)
{
Unsafe.Add(ref suffixRef, (uint)code) = (byte)code;
}
Span<byte> buffer = stackalloc byte[byte.MaxValue];
int y = 0;
int x = 0; int x = 0;
int rowMax = width; int xyz = 0;
ref byte pixelsRowRef = ref MemoryMarshal.GetReference(pixels.DangerousGetRowSpan(y)); while (xyz < indices.Length)
while (xyz < length)
{ {
// Reset row reference. if (this.top == 0)
if (xyz == rowMax)
{
x = 0;
pixelsRowRef = ref MemoryMarshal.GetReference(pixels.DangerousGetRowSpan(++y));
rowMax = (y * width) + width;
}
if (top == 0)
{ {
if (bits < codeSize) if (this.bits < this.codeSize)
{ {
// Load bytes until there are enough bits for a code. // Load bytes until there are enough bits for a code.
if (count == 0) if (this.count == 0)
{ {
// Read a new data block. // Read a new data block.
count = this.ReadBlock(buffer); this.count = this.ReadBlock(buffer);
if (count == 0) if (this.count == 0)
{ {
break; break;
} }
bi = 0; this.bufferIndex = 0;
} }
data += buffer[bi] << bits; this.data += buffer[this.bufferIndex] << this.bits;
bits += 8; this.bits += 8;
bi++; this.bufferIndex++;
count--; this.count--;
continue; continue;
} }
// Get the next code // Get the next code
code = data & codeMask; this.code = this.data & this.codeMask;
data >>= codeSize; this.data >>= this.codeSize;
bits -= codeSize; this.bits -= this.codeSize;
// Interpret the code // Interpret the code
if (code > availableCode || code == endCode) if (this.code > this.availableCode || this.code == this.endCode)
{ {
break; break;
} }
if (code == clearCode) if (this.code == this.clearCode)
{ {
// Reset the decoder // Reset the decoder
codeSize = minCodeSize + 1; this.codeSize = this.minCodeSize + 1;
codeMask = (1 << codeSize) - 1; this.codeMask = (1 << this.codeSize) - 1;
availableCode = clearCode + 2; this.availableCode = this.clearCode + 2;
oldCode = NullCode; this.oldCode = NullCode;
continue; continue;
} }
if (oldCode == NullCode) if (this.oldCode == NullCode)
{ {
Unsafe.Add(ref pixelStackRef, (uint)top++) = Unsafe.Add(ref suffixRef, (uint)code); Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code);
oldCode = code; this.oldCode = this.code;
first = code; this.first = this.code;
continue; continue;
} }
int inCode = code; int inCode = this.code;
if (code == availableCode) 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); Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code);
code = Unsafe.Add(ref prefixRef, (uint)code); this.code = Unsafe.Add(ref prefixRef, (uint)this.code);
} }
int suffixCode = Unsafe.Add(ref suffixRef, (uint)code); int suffixCode = Unsafe.Add(ref suffixRef, (uint)this.code);
first = suffixCode; this.first = suffixCode;
Unsafe.Add(ref pixelStackRef, (uint)top++) = suffixCode; Unsafe.Add(ref pixelStackRef, (uint)this.top++) = suffixCode;
// Fix for Gifs that have "deferred clear code" as per here : // Fix for Gifs that have "deferred clear code" as per here :
// https://bugzilla.mozilla.org/show_bug.cgi?id=55918 // 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 prefixRef, (uint)this.availableCode) = this.oldCode;
Unsafe.Add(ref suffixRef, (uint)availableCode) = first; Unsafe.Add(ref suffixRef, (uint)this.availableCode) = this.first;
availableCode++; this.availableCode++;
if (availableCode == codeMask + 1 && availableCode < MaxStackSize) if (this.availableCode == this.codeMask + 1 && this.availableCode < MaxStackSize)
{ {
codeSize++; this.codeSize++;
codeMask = (1 << codeSize) - 1; this.codeMask = (1 << this.codeSize) - 1;
} }
} }
oldCode = inCode; this.oldCode = inCode;
} }
// Pop a pixel off the pixel stack. // Pop a pixel off the pixel stack.
top--; this.top--;
// Clear missing pixels // Clear missing pixels
xyz++; 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);
} }
} }
/// <summary> /// <summary>
/// Decodes and decompresses all pixel indices from the stream allowing skipping of the data. /// Decodes and decompresses all pixel indices from the stream allowing skipping of the data.
/// </summary> /// </summary>
/// <param name="minCodeSize">Minimum code size of the data.</param>
/// <param name="length">The resulting index table length.</param> /// <param name="length">The resulting index table length.</param>
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 prefixRef = ref MemoryMarshal.GetReference(this.prefix.GetSpan());
ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan()); ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan());
ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan()); ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan());
Span<byte> buffer = this.scratchBuffer.GetSpan();
for (code = 0; code < clearCode; code++) int xyz = 0;
{
Unsafe.Add(ref suffixRef, (uint)code) = (byte)code;
}
Span<byte> buffer = stackalloc byte[byte.MaxValue];
while (xyz < length) 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. // Load bytes until there are enough bits for a code.
if (count == 0) if (this.count == 0)
{ {
// Read a new data block. // Read a new data block.
count = this.ReadBlock(buffer); this.count = this.ReadBlock(buffer);
if (count == 0) if (this.count == 0)
{ {
break; break;
} }
bi = 0; this.bufferIndex = 0;
} }
data += buffer[bi] << bits; this.data += buffer[this.bufferIndex] << this.bits;
bits += 8; this.bits += 8;
bi++; this.bufferIndex++;
count--; this.count--;
continue; continue;
} }
// Get the next code // Get the next code
code = data & codeMask; this.code = this.data & this.codeMask;
data >>= codeSize; this.data >>= this.codeSize;
bits -= codeSize; this.bits -= this.codeSize;
// Interpret the code // Interpret the code
if (code > availableCode || code == endCode) if (this.code > this.availableCode || this.code == this.endCode)
{ {
break; break;
} }
if (code == clearCode) if (this.code == this.clearCode)
{ {
// Reset the decoder // Reset the decoder
codeSize = minCodeSize + 1; this.codeSize = this.minCodeSize + 1;
codeMask = (1 << codeSize) - 1; this.codeMask = (1 << this.codeSize) - 1;
availableCode = clearCode + 2; this.availableCode = this.clearCode + 2;
oldCode = NullCode; this.oldCode = NullCode;
continue; continue;
} }
if (oldCode == NullCode) if (this.oldCode == NullCode)
{ {
Unsafe.Add(ref pixelStackRef, (uint)top++) = Unsafe.Add(ref suffixRef, (uint)code); Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code);
oldCode = code; this.oldCode = this.code;
first = code; this.first = this.code;
continue; continue;
} }
int inCode = code; int inCode = this.code;
if (code == availableCode) 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); Unsafe.Add(ref pixelStackRef, (uint)this.top++) = Unsafe.Add(ref suffixRef, (uint)this.code);
code = Unsafe.Add(ref prefixRef, (uint)code); this.code = Unsafe.Add(ref prefixRef, (uint)this.code);
} }
int suffixCode = Unsafe.Add(ref suffixRef, (uint)code); int suffixCode = Unsafe.Add(ref suffixRef, (uint)this.code);
first = suffixCode; this.first = suffixCode;
Unsafe.Add(ref pixelStackRef, (uint)top++) = suffixCode; Unsafe.Add(ref pixelStackRef, (uint)this.top++) = suffixCode;
// Fix for Gifs that have "deferred clear code" as per here : // Fix for Gifs that have "deferred clear code" as per here :
// https://bugzilla.mozilla.org/show_bug.cgi?id=55918 // 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 prefixRef, (uint)this.availableCode) = this.oldCode;
Unsafe.Add(ref suffixRef, (uint)availableCode) = first; Unsafe.Add(ref suffixRef, (uint)this.availableCode) = this.first;
availableCode++; this.availableCode++;
if (availableCode == codeMask + 1 && availableCode < MaxStackSize) if (this.availableCode == this.codeMask + 1 && this.availableCode < MaxStackSize)
{ {
codeSize++; this.codeSize++;
codeMask = (1 << codeSize) - 1; this.codeMask = (1 << this.codeSize) - 1;
} }
} }
oldCode = inCode; this.oldCode = inCode;
} }
// Pop a pixel off the pixel stack. // Pop a pixel off the pixel stack.
top--; this.top--;
// Clear missing pixels // Clear missing pixels
xyz++; xyz++;
@ -419,5 +380,6 @@ internal sealed class LzwDecoder : IDisposable
this.prefix.Dispose(); this.prefix.Dispose();
this.suffix.Dispose(); this.suffix.Dispose();
this.pixelStack.Dispose(); this.pixelStack.Dispose();
this.scratchBuffer.Dispose();
} }
} }

2
src/ImageSharp/Formats/Jpeg/Components/Quantization.cs

@ -146,7 +146,7 @@ internal static class Quantization
quality = (int)Math.Round(5000.0 / sumPercent); quality = (int)Math.Round(5000.0 / sumPercent);
} }
return quality; return Numerics.Clamp(quality, MinQualityFactor, MaxQualityFactor);
} }
/// <summary> /// <summary>

11
tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs

@ -204,6 +204,17 @@ public class GifDecoderTests
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
} }
// https://github.com/SixLabors/ImageSharp/issues/2758
[Theory]
[WithFile(TestImages.Gif.Issues.Issue2758, PixelTypes.Rgba32)]
public void Issue2758_BadDescriptorDimensions<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
image.DebugSaveMultiFrame(provider);
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
}
// https://github.com/SixLabors/ImageSharp/issues/405 // https://github.com/SixLabors/ImageSharp/issues/405
[Theory] [Theory]
[WithFile(TestImages.Gif.Issues.BadAppExtLength, PixelTypes.Rgba32)] [WithFile(TestImages.Gif.Issues.BadAppExtLength, PixelTypes.Rgba32)]

28
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs

@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
@ -438,6 +439,33 @@ public partial class JpegDecoderTests
Assert.Equal(expectedComment, metadata.Comments.ElementAtOrDefault(0).ToString()); Assert.Equal(expectedComment, metadata.Comments.ElementAtOrDefault(0).ToString());
image.DebugSave(provider); image.DebugSave(provider);
image.CompareToOriginal(provider); image.CompareToOriginal(provider);
}
// https://github.com/SixLabors/ImageSharp/issues/2758
[Theory]
[WithFile(TestImages.Jpeg.Issues.Issue2758, PixelTypes.L8)]
public void Issue2758_DecodeWorks<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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) private static void VerifyEncodedStrings(ExifProfile exif)

2
tests/ImageSharp.Tests/TestImages.cs

@ -321,6 +321,7 @@ public static class TestImages
public const string Issue2517 = "Jpg/issues/issue2517-bad-d7.jpg"; public const string Issue2517 = "Jpg/issues/issue2517-bad-d7.jpg";
public const string Issue2067_CommentMarker = "Jpg/issues/issue-2067-comment.jpg"; public const string Issue2067_CommentMarker = "Jpg/issues/issue-2067-comment.jpg";
public const string Issue2638 = "Jpg/issues/Issue2638.jpg"; public const string Issue2638 = "Jpg/issues/Issue2638.jpg";
public const string Issue2758 = "Jpg/issues/issue-2758.jpg";
public static class Fuzz public static class Fuzz
{ {
@ -531,6 +532,7 @@ public static class TestImages
public const string Issue2450_A = "Gif/issues/issue_2450.gif"; public const string Issue2450_A = "Gif/issues/issue_2450.gif";
public const string Issue2450_B = "Gif/issues/issue_2450_2.gif"; public const string Issue2450_B = "Gif/issues/issue_2450_2.gif";
public const string Issue2198 = "Gif/issues/issue_2198.gif"; public const string Issue2198 = "Gif/issues/issue_2198.gif";
public const string Issue2758 = "Gif/issues/issue_2758.gif";
} }
public static readonly string[] Animated = public static readonly string[] Animated =

4
tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012BadMinCode_Rgba32_issue2012_drona1.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a3a24c066895fd3a76649da376485cbc1912d6a3ae15369575f523e66364b3b6 oid sha256:588d055a93c7b4fdb62e8b77f3ae08753a9e8990151cb0523f5e761996189b70
size 141563 size 142244

3
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

3
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

4
tests/Images/External/ReferenceOutput/GifDecoderTests/IssueTooLargeLzwBits_Rgba32_issue_2743.gif/07.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:489642f0c81fd12e97007fe6feb11b0e93e351199a922ce038069a3782ad0722 oid sha256:5016a323018f09e292165ad5392d82dcbad5e79c2b6b93aff3322dffff80b309
size 135 size 126

3
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

3
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
Loading…
Cancel
Save