Skip to content

Commit e6f08f0

Browse files
authored
Merge branch 'main' into bp/tiffonlyfirstframe
2 parents 6016c5c + 3259c94 commit e6f08f0

27 files changed

+251
-83
lines changed

ImageSharp.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Gif", "Gif", "{EE3FB0B3-1C3
142142
EndProject
143143
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "issues", "issues", "{BF8DFDC1-CEE5-4A37-B216-D3085360C776}"
144144
ProjectSection(SolutionItems) = preProject
145+
tests\Images\Input\Gif\issues\bugzilla-55918.gif = tests\Images\Input\Gif\issues\bugzilla-55918.gif
146+
tests\Images\Input\Gif\issues\issue1505_argumentoutofrange.png = tests\Images\Input\Gif\issues\issue1505_argumentoutofrange.png
147+
tests\Images\Input\Gif\issues\issue1530.gif = tests\Images\Input\Gif\issues\issue1530.gif
148+
tests\Images\Input\Gif\issues\issue1668_invalidcolorindex.gif = tests\Images\Input\Gif\issues\issue1668_invalidcolorindex.gif
149+
tests\Images\Input\Gif\issues\issue1962_tiniest_gif_1st.gif = tests\Images\Input\Gif\issues\issue1962_tiniest_gif_1st.gif
150+
tests\Images\Input\Gif\issues\issue2012_drona1.gif = tests\Images\Input\Gif\issues\issue2012_drona1.gif
151+
tests\Images\Input\Gif\issues\issue2012_Stronghold-Crusader-Extreme-Cover.gif = tests\Images\Input\Gif\issues\issue2012_Stronghold-Crusader-Extreme-Cover.gif
145152
tests\Images\Input\Gif\issues\issue403_baddescriptorwidth.gif = tests\Images\Input\Gif\issues\issue403_baddescriptorwidth.gif
146153
tests\Images\Input\Gif\issues\issue405_badappextlength252-2.gif = tests\Images\Input\Gif\issues\issue405_badappextlength252-2.gif
147154
tests\Images\Input\Gif\issues\issue405_badappextlength252.gif = tests\Images\Input\Gif\issues\issue405_badappextlength252.gif

src/ImageSharp/Common/Extensions/StreamExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp
1313
internal static class StreamExtensions
1414
{
1515
/// <summary>
16-
/// Writes data from a stream into the provided buffer.
16+
/// Writes data from a stream from the provided buffer.
1717
/// </summary>
1818
/// <param name="stream">The stream.</param>
1919
/// <param name="buffer">The buffer.</param>

src/ImageSharp/Formats/Gif/GifDecoderCore.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -265,10 +265,14 @@ private void ReadApplicationExtension()
265265
this.stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize);
266266
bool isXmp = this.buffer.AsSpan().StartsWith(GifConstants.XmpApplicationIdentificationBytes);
267267

268-
if (isXmp)
268+
if (isXmp && !this.IgnoreMetadata)
269269
{
270-
var extension = GifXmpApplicationExtension.Read(this.stream);
271-
this.metadata.XmpProfile = new XmpProfile(extension.Data);
270+
var extension = GifXmpApplicationExtension.Read(this.stream, this.MemoryAllocator);
271+
if (extension.Data.Length > 0)
272+
{
273+
this.metadata.XmpProfile = new XmpProfile(extension.Data);
274+
}
275+
272276
return;
273277
}
274278
else
@@ -374,8 +378,8 @@ private void ReadFrame<TPixel>(ref Image<TPixel> image, ref ImageFrame<TPixel> p
374378
}
375379

376380
indices = this.Configuration.MemoryAllocator.Allocate2D<byte>(this.imageDescriptor.Width, this.imageDescriptor.Height, AllocationOptions.Clean);
377-
378381
this.ReadFrameIndices(indices);
382+
379383
Span<byte> rawColorTable = default;
380384
if (localColorTable != null)
381385
{
@@ -406,9 +410,9 @@ private void ReadFrame<TPixel>(ref Image<TPixel> image, ref ImageFrame<TPixel> p
406410
[MethodImpl(MethodImplOptions.AggressiveInlining)]
407411
private void ReadFrameIndices(Buffer2D<byte> indices)
408412
{
409-
int dataSize = this.stream.ReadByte();
413+
int minCodeSize = this.stream.ReadByte();
410414
using var lzwDecoder = new LzwDecoder(this.Configuration.MemoryAllocator, this.stream);
411-
lzwDecoder.DecodePixels(dataSize, indices);
415+
lzwDecoder.DecodePixels(minCodeSize, indices);
412416
}
413417

414418
/// <summary>

src/ImageSharp/Formats/Gif/GifEncoderCore.cs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
123123
this.WriteComments(gifMetadata, stream);
124124

125125
// Write application extensions.
126-
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, metadata.XmpProfile);
126+
XmpProfile xmpProfile = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile;
127+
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile);
127128

128129
if (useGlobalTable)
129130
{
@@ -137,7 +138,6 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
137138
// Clean up.
138139
quantized.Dispose();
139140

140-
// TODO: Write extension etc
141141
stream.WriteByte(GifConstants.EndIntroducer);
142142
}
143143

@@ -428,26 +428,31 @@ private void WriteExtension<TGifExtension>(TGifExtension extension, Stream strea
428428
where TGifExtension : struct, IGifExtension
429429
{
430430
IMemoryOwner<byte> owner = null;
431-
Span<byte> buffer;
431+
Span<byte> extensionBuffer;
432432
int extensionSize = extension.ContentLength;
433-
if (extensionSize > this.buffer.Length - 3)
433+
434+
if (extensionSize == 0)
435+
{
436+
return;
437+
}
438+
else if (extensionSize > this.buffer.Length - 3)
434439
{
435440
owner = this.memoryAllocator.Allocate<byte>(extensionSize + 3);
436-
buffer = owner.GetSpan();
441+
extensionBuffer = owner.GetSpan();
437442
}
438443
else
439444
{
440-
buffer = this.buffer;
445+
extensionBuffer = this.buffer;
441446
}
442447

443-
buffer[0] = GifConstants.ExtensionIntroducer;
444-
buffer[1] = extension.Label;
448+
extensionBuffer[0] = GifConstants.ExtensionIntroducer;
449+
extensionBuffer[1] = extension.Label;
445450

446-
extension.WriteTo(buffer.Slice(2));
451+
extension.WriteTo(extensionBuffer.Slice(2));
447452

448-
buffer[extensionSize + 2] = GifConstants.Terminator;
453+
extensionBuffer[extensionSize + 2] = GifConstants.Terminator;
449454

450-
stream.Write(buffer, 0, extensionSize + 3);
455+
stream.Write(extensionBuffer, 0, extensionSize + 3);
451456
owner?.Dispose();
452457
}
453458

src/ImageSharp/Formats/Gif/LzwDecoder.cs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,21 +64,30 @@ public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream)
6464
/// <summary>
6565
/// Decodes and decompresses all pixel indices from the stream.
6666
/// </summary>
67-
/// <param name="dataSize">Size of the data.</param>
67+
/// <param name="minCodeSize">Minimum code size of the data.</param>
6868
/// <param name="pixels">The pixel array to decode to.</param>
69-
public void DecodePixels(int dataSize, Buffer2D<byte> pixels)
69+
public void DecodePixels(int minCodeSize, Buffer2D<byte> pixels)
7070
{
71-
Guard.MustBeLessThan(dataSize, int.MaxValue, nameof(dataSize));
71+
// Calculate the clear code. The value of the clear code is 2 ^ minCodeSize
72+
int clearCode = 1 << minCodeSize;
73+
74+
// It is possible to specify a larger LZW minimum code size than the palette length in bits
75+
// which may leave a gap in the codes where no colors are assigned.
76+
// http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp#lzw_compression
77+
if (minCodeSize < 2 || clearCode > MaxStackSize)
78+
{
79+
// Don't attempt to decode the frame indices.
80+
// Theoretically we could determine a min code size from the length of the provided
81+
// color palette but we won't bother since the image is most likely corrupted.
82+
GifThrowHelper.ThrowInvalidImageContentException("Gif Image does not contain a valid LZW minimum code.");
83+
}
7284

7385
// The resulting index table length.
7486
int width = pixels.Width;
7587
int height = pixels.Height;
7688
int length = width * height;
7789

78-
// Calculate the clear code. The value of the clear code is 2 ^ dataSize
79-
int clearCode = 1 << dataSize;
80-
81-
int codeSize = dataSize + 1;
90+
int codeSize = minCodeSize + 1;
8291

8392
// Calculate the end code
8493
int endCode = clearCode + 1;
@@ -165,7 +174,7 @@ public void DecodePixels(int dataSize, Buffer2D<byte> pixels)
165174
if (code == clearCode)
166175
{
167176
// Reset the decoder
168-
codeSize = dataSize + 1;
177+
codeSize = minCodeSize + 1;
169178
codeMask = (1 << codeSize) - 1;
170179
availableCode = clearCode + 2;
171180
oldCode = NullCode;

src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,11 @@ public int WriteTo(Span<byte> buffer)
7171

7272
dest = this;
7373

74-
return 5;
74+
return ((IGifExtension)this).ContentLength;
7575
}
7676

7777
public static GifGraphicControlExtension Parse(ReadOnlySpan<byte> buffer)
78-
{
79-
return MemoryMarshal.Cast<byte, GifGraphicControlExtension>(buffer)[0];
80-
}
78+
=> MemoryMarshal.Cast<byte, GifGraphicControlExtension>(buffer)[0];
8179

8280
public static byte GetPackedValue(GifDisposalMethod disposalMethod, bool userInputFlag = false, bool transparencyFlag = false)
8381
{

src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public int WriteTo(Span<byte> buffer)
4040
// 0 means loop indefinitely. Count is set as play n + 1 times.
4141
BinaryPrimitives.WriteUInt16LittleEndian(buffer.Slice(14, 2), this.RepeatCount);
4242

43-
return 16; // Length - Introducer + Label + Terminator.
43+
return this.ContentLength; // Length - Introducer + Label + Terminator.
4444
}
4545
}
4646
}

src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs

Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
// Licensed under the Apache License, Version 2.0.
33

44
using System;
5-
using System.Collections.Generic;
65
using System.IO;
7-
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
6+
using SixLabors.ImageSharp.IO;
7+
using SixLabors.ImageSharp.Memory;
88

99
namespace SixLabors.ImageSharp.Formats.Gif
1010
{
@@ -14,7 +14,10 @@ namespace SixLabors.ImageSharp.Formats.Gif
1414

1515
public byte Label => GifConstants.ApplicationExtensionLabel;
1616

17-
public int ContentLength => this.Data.Length + 269; // 12 + Data Length + 1 + 256
17+
// size : 1
18+
// identifier : 11
19+
// magic trailer : 257
20+
public int ContentLength => (this.Data.Length > 0) ? this.Data.Length + 269 : 0;
1821

1922
/// <summary>
2023
/// Gets the raw Data.
@@ -25,51 +28,28 @@ namespace SixLabors.ImageSharp.Formats.Gif
2528
/// Reads the XMP metadata from the specified stream.
2629
/// </summary>
2730
/// <param name="stream">The stream to read from.</param>
31+
/// <param name="allocator">The memory allocator.</param>
2832
/// <returns>The XMP metadata</returns>
2933
/// <exception cref="ImageFormatException">Thrown if the XMP block is not properly terminated.</exception>
30-
public static GifXmpApplicationExtension Read(Stream stream)
34+
public static GifXmpApplicationExtension Read(Stream stream, MemoryAllocator allocator)
3135
{
32-
// Read data in blocks, until an \0 character is encountered.
33-
// We overshoot, indicated by the terminatorIndex variable.
34-
const int bufferSize = 256;
35-
var list = new List<byte[]>();
36-
int terminationIndex = -1;
37-
while (terminationIndex < 0)
38-
{
39-
byte[] temp = new byte[bufferSize];
40-
int bytesRead = stream.Read(temp);
41-
list.Add(temp);
42-
terminationIndex = Array.IndexOf(temp, (byte)1);
43-
}
36+
byte[] xmpBytes = ReadXmpData(stream, allocator);
4437

45-
// Pack all the blocks (except magic trailer) into one single array again.
46-
int dataSize = ((list.Count - 1) * bufferSize) + terminationIndex;
47-
byte[] buffer = new byte[dataSize];
48-
Span<byte> bufferSpan = buffer;
49-
int pos = 0;
50-
for (int j = 0; j < list.Count - 1; j++)
38+
// Exclude the "magic trailer", see XMP Specification Part 3, 1.1.2 GIF
39+
int xmpLength = xmpBytes.Length - 256; // 257 - unread 0x0
40+
byte[] buffer = Array.Empty<byte>();
41+
if (xmpLength > 0)
5142
{
52-
list[j].CopyTo(bufferSpan.Slice(pos));
53-
pos += bufferSize;
43+
buffer = new byte[xmpLength];
44+
xmpBytes.AsSpan(0, xmpLength).CopyTo(buffer);
45+
stream.Skip(1); // Skip the terminator.
5446
}
5547

56-
// Last one only needs the portion until terminationIndex copied over.
57-
Span<byte> lastBytes = list[list.Count - 1];
58-
lastBytes.Slice(0, terminationIndex).CopyTo(bufferSpan.Slice(pos));
59-
60-
// Skip the remainder of the magic trailer.
61-
stream.Skip(258 - (bufferSize - terminationIndex));
6248
return new GifXmpApplicationExtension(buffer);
6349
}
6450

6551
public int WriteTo(Span<byte> buffer)
6652
{
67-
int totalSize = this.ContentLength;
68-
if (buffer.Length < totalSize)
69-
{
70-
throw new InsufficientMemoryException("Unable to write XMP metadata to GIF image");
71-
}
72-
7353
int bytesWritten = 0;
7454
buffer[bytesWritten++] = GifConstants.ApplicationBlockSize;
7555

@@ -91,7 +71,28 @@ public int WriteTo(Span<byte> buffer)
9171

9272
buffer[bytesWritten++] = 0x00;
9373

94-
return totalSize;
74+
return this.ContentLength;
75+
}
76+
77+
private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator)
78+
{
79+
using ChunkedMemoryStream bytes = new(allocator);
80+
81+
// XMP data doesn't have a fixed length nor is there an indicator of the length.
82+
// So we simply read one byte at a time until we hit the 0x0 value at the end
83+
// of the magic trailer or the end of the stream.
84+
// Using ChunkedMemoryStream reduces the array resize allocation normally associated
85+
// with writing from a non fixed-size buffer.
86+
while (true)
87+
{
88+
int b = stream.ReadByte();
89+
if (b <= 0)
90+
{
91+
return bytes.ToArray();
92+
}
93+
94+
bytes.WriteByte((byte)b);
95+
}
9596
}
9697
}
9798
}

src/ImageSharp/Formats/Png/PngDecoderCore.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -429,10 +429,17 @@ private void ReadPhysicalChunk(ImageMetadata metadata, ReadOnlySpan<byte> data)
429429
/// <param name="pngMetadata">The metadata to read to.</param>
430430
/// <param name="data">The data containing physical data.</param>
431431
private void ReadGammaChunk(PngMetadata pngMetadata, ReadOnlySpan<byte> data)
432+
{
433+
if (data.Length < 4)
434+
{
435+
// Ignore invalid gamma chunks.
436+
return;
437+
}
432438

433-
// The value is encoded as a 4-byte unsigned integer, representing gamma times 100000.
434439
// For example, a gamma of 1/2.2 would be stored as 45455.
435-
=> pngMetadata.Gamma = BinaryPrimitives.ReadUInt32BigEndian(data) * 1e-5F;
440+
// The value is encoded as a 4-byte unsigned integer, representing gamma times 100000.
441+
pngMetadata.Gamma = BinaryPrimitives.ReadUInt32BigEndian(data) * 1e-5F;
442+
}
436443

437444
/// <summary>
438445
/// Initializes the image and various buffers needed for processing

src/ImageSharp/Formats/Png/PngScanlineProcessor.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,11 @@ public static void ProcessPaletteScanline<TPixel>(
240240
byte[] paletteAlpha)
241241
where TPixel : unmanaged, IPixel<TPixel>
242242
{
243+
if (palette.IsEmpty)
244+
{
245+
PngThrowHelper.ThrowMissingPalette();
246+
}
247+
243248
TPixel pixel = default;
244249
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
245250
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);

0 commit comments

Comments
 (0)