Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions ImageSharp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Gif", "Gif", "{EE3FB0B3-1C3
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "issues", "issues", "{BF8DFDC1-CEE5-4A37-B216-D3085360C776}"
ProjectSection(SolutionItems) = preProject
tests\Images\Input\Gif\issues\bugzilla-55918.gif = tests\Images\Input\Gif\issues\bugzilla-55918.gif
tests\Images\Input\Gif\issues\issue1505_argumentoutofrange.png = tests\Images\Input\Gif\issues\issue1505_argumentoutofrange.png
tests\Images\Input\Gif\issues\issue1530.gif = tests\Images\Input\Gif\issues\issue1530.gif
tests\Images\Input\Gif\issues\issue1668_invalidcolorindex.gif = tests\Images\Input\Gif\issues\issue1668_invalidcolorindex.gif
tests\Images\Input\Gif\issues\issue1962_tiniest_gif_1st.gif = tests\Images\Input\Gif\issues\issue1962_tiniest_gif_1st.gif
tests\Images\Input\Gif\issues\issue2012_drona1.gif = tests\Images\Input\Gif\issues\issue2012_drona1.gif
tests\Images\Input\Gif\issues\issue2012_Stronghold-Crusader-Extreme-Cover.gif = tests\Images\Input\Gif\issues\issue2012_Stronghold-Crusader-Extreme-Cover.gif
tests\Images\Input\Gif\issues\issue403_baddescriptorwidth.gif = tests\Images\Input\Gif\issues\issue403_baddescriptorwidth.gif
tests\Images\Input\Gif\issues\issue405_badappextlength252-2.gif = tests\Images\Input\Gif\issues\issue405_badappextlength252-2.gif
tests\Images\Input\Gif\issues\issue405_badappextlength252.gif = tests\Images\Input\Gif\issues\issue405_badappextlength252.gif
Expand Down
2 changes: 1 addition & 1 deletion src/ImageSharp/Common/Extensions/StreamExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp
internal static class StreamExtensions
{
/// <summary>
/// Writes data from a stream into the provided buffer.
/// Writes data from a stream from the provided buffer.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="buffer">The buffer.</param>
Expand Down
16 changes: 10 additions & 6 deletions src/ImageSharp/Formats/Gif/GifDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,10 +265,14 @@ private void ReadApplicationExtension()
this.stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize);
bool isXmp = this.buffer.AsSpan().StartsWith(GifConstants.XmpApplicationIdentificationBytes);

if (isXmp)
if (isXmp && !this.IgnoreMetadata)
{
var extension = GifXmpApplicationExtension.Read(this.stream);
this.metadata.XmpProfile = new XmpProfile(extension.Data);
var extension = GifXmpApplicationExtension.Read(this.stream, this.MemoryAllocator);
if (extension.Data.Length > 0)
{
this.metadata.XmpProfile = new XmpProfile(extension.Data);
}

return;
}
else
Expand Down Expand Up @@ -374,8 +378,8 @@ private void ReadFrame<TPixel>(ref Image<TPixel> image, ref ImageFrame<TPixel> p
}

indices = this.Configuration.MemoryAllocator.Allocate2D<byte>(this.imageDescriptor.Width, this.imageDescriptor.Height, AllocationOptions.Clean);

this.ReadFrameIndices(indices);

Span<byte> rawColorTable = default;
if (localColorTable != null)
{
Expand Down Expand Up @@ -406,9 +410,9 @@ private void ReadFrame<TPixel>(ref Image<TPixel> image, ref ImageFrame<TPixel> p
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ReadFrameIndices(Buffer2D<byte> indices)
{
int dataSize = this.stream.ReadByte();
int minCodeSize = this.stream.ReadByte();
using var lzwDecoder = new LzwDecoder(this.Configuration.MemoryAllocator, this.stream);
lzwDecoder.DecodePixels(dataSize, indices);
lzwDecoder.DecodePixels(minCodeSize, indices);
}

/// <summary>
Expand Down
27 changes: 16 additions & 11 deletions src/ImageSharp/Formats/Gif/GifEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
this.WriteComments(gifMetadata, stream);

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

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

// TODO: Write extension etc
stream.WriteByte(GifConstants.EndIntroducer);
}

Expand Down Expand Up @@ -428,26 +428,31 @@ private void WriteExtension<TGifExtension>(TGifExtension extension, Stream strea
where TGifExtension : struct, IGifExtension
{
IMemoryOwner<byte> owner = null;
Span<byte> buffer;
Span<byte> extensionBuffer;
int extensionSize = extension.ContentLength;
if (extensionSize > this.buffer.Length - 3)

if (extensionSize == 0)
{
return;
}
else if (extensionSize > this.buffer.Length - 3)
{
owner = this.memoryAllocator.Allocate<byte>(extensionSize + 3);
buffer = owner.GetSpan();
extensionBuffer = owner.GetSpan();
}
else
{
buffer = this.buffer;
extensionBuffer = this.buffer;
}

buffer[0] = GifConstants.ExtensionIntroducer;
buffer[1] = extension.Label;
extensionBuffer[0] = GifConstants.ExtensionIntroducer;
extensionBuffer[1] = extension.Label;

extension.WriteTo(buffer.Slice(2));
extension.WriteTo(extensionBuffer.Slice(2));

buffer[extensionSize + 2] = GifConstants.Terminator;
extensionBuffer[extensionSize + 2] = GifConstants.Terminator;

stream.Write(buffer, 0, extensionSize + 3);
stream.Write(extensionBuffer, 0, extensionSize + 3);
owner?.Dispose();
}

Expand Down
25 changes: 17 additions & 8 deletions src/ImageSharp/Formats/Gif/LzwDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,21 +64,30 @@ public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream)
/// <summary>
/// Decodes and decompresses all pixel indices from the stream.
/// </summary>
/// <param name="dataSize">Size of the data.</param>
/// <param name="minCodeSize">Minimum code size of the data.</param>
/// <param name="pixels">The pixel array to decode to.</param>
public void DecodePixels(int dataSize, Buffer2D<byte> pixels)
public void DecodePixels(int minCodeSize, Buffer2D<byte> pixels)
{
Guard.MustBeLessThan(dataSize, int.MaxValue, nameof(dataSize));
// 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 || 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.
GifThrowHelper.ThrowInvalidImageContentException("Gif Image does not contain a valid LZW minimum code.");
}

// The resulting index table length.
int width = pixels.Width;
int height = pixels.Height;
int length = width * height;

// Calculate the clear code. The value of the clear code is 2 ^ dataSize
int clearCode = 1 << dataSize;

int codeSize = dataSize + 1;
int codeSize = minCodeSize + 1;

// Calculate the end code
int endCode = clearCode + 1;
Expand Down Expand Up @@ -165,7 +174,7 @@ public void DecodePixels(int dataSize, Buffer2D<byte> pixels)
if (code == clearCode)
{
// Reset the decoder
codeSize = dataSize + 1;
codeSize = minCodeSize + 1;
codeMask = (1 << codeSize) - 1;
availableCode = clearCode + 2;
oldCode = NullCode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,11 @@ public int WriteTo(Span<byte> buffer)

dest = this;

return 5;
return ((IGifExtension)this).ContentLength;
}

public static GifGraphicControlExtension Parse(ReadOnlySpan<byte> buffer)
{
return MemoryMarshal.Cast<byte, GifGraphicControlExtension>(buffer)[0];
}
=> MemoryMarshal.Cast<byte, GifGraphicControlExtension>(buffer)[0];

public static byte GetPackedValue(GifDisposalMethod disposalMethod, bool userInputFlag = false, bool transparencyFlag = false)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public int WriteTo(Span<byte> buffer)
// 0 means loop indefinitely. Count is set as play n + 1 times.
BinaryPrimitives.WriteUInt16LittleEndian(buffer.Slice(14, 2), this.RepeatCount);

return 16; // Length - Introducer + Label + Terminator.
return this.ContentLength; // Length - Introducer + Label + Terminator.
}
}
}
75 changes: 38 additions & 37 deletions src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
// Licensed under the Apache License, Version 2.0.

using System;
using System.Collections.Generic;
using System.IO;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;

namespace SixLabors.ImageSharp.Formats.Gif
{
Expand All @@ -14,7 +14,10 @@ namespace SixLabors.ImageSharp.Formats.Gif

public byte Label => GifConstants.ApplicationExtensionLabel;

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

/// <summary>
/// Gets the raw Data.
Expand All @@ -25,51 +28,28 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Reads the XMP metadata from the specified stream.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="allocator">The memory allocator.</param>
/// <returns>The XMP metadata</returns>
/// <exception cref="ImageFormatException">Thrown if the XMP block is not properly terminated.</exception>
public static GifXmpApplicationExtension Read(Stream stream)
public static GifXmpApplicationExtension Read(Stream stream, MemoryAllocator allocator)
{
// Read data in blocks, until an \0 character is encountered.
// We overshoot, indicated by the terminatorIndex variable.
const int bufferSize = 256;
var list = new List<byte[]>();
int terminationIndex = -1;
while (terminationIndex < 0)
{
byte[] temp = new byte[bufferSize];
int bytesRead = stream.Read(temp);
list.Add(temp);
terminationIndex = Array.IndexOf(temp, (byte)1);
}
byte[] xmpBytes = ReadXmpData(stream, allocator);

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

// Last one only needs the portion until terminationIndex copied over.
Span<byte> lastBytes = list[list.Count - 1];
lastBytes.Slice(0, terminationIndex).CopyTo(bufferSpan.Slice(pos));

// Skip the remainder of the magic trailer.
stream.Skip(258 - (bufferSize - terminationIndex));
return new GifXmpApplicationExtension(buffer);
}

public int WriteTo(Span<byte> buffer)
{
int totalSize = this.ContentLength;
if (buffer.Length < totalSize)
{
throw new InsufficientMemoryException("Unable to write XMP metadata to GIF image");
}

int bytesWritten = 0;
buffer[bytesWritten++] = GifConstants.ApplicationBlockSize;

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

buffer[bytesWritten++] = 0x00;

return totalSize;
return this.ContentLength;
}

private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator)
{
using ChunkedMemoryStream bytes = new(allocator);

// XMP data doesn't have a fixed length nor is there an indicator of the length.
// So we simply read one byte at a time until we hit the 0x0 value at the end
// of the magic trailer or the end of the stream.
// Using ChunkedMemoryStream reduces the array resize allocation normally associated
// with writing from a non fixed-size buffer.
while (true)
{
int b = stream.ReadByte();
if (b <= 0)
{
return bytes.ToArray();
}

bytes.WriteByte((byte)b);
}
}
}
}
3 changes: 2 additions & 1 deletion src/ImageSharp/Metadata/ImageMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ private ImageMetadata(ImageMetadata other)
this.ExifProfile = other.ExifProfile?.DeepClone();
this.IccProfile = other.IccProfile?.DeepClone();
this.IptcProfile = other.IptcProfile?.DeepClone();
this.XmpProfile = other.XmpProfile?.DeepClone();
}

/// <summary>
Expand Down Expand Up @@ -175,7 +176,7 @@ public TFormatMetadata GetFormatMetadata<TFormatMetadata>(IImageFormat<TFormatMe
}

/// <inheritdoc/>
public ImageMetadata DeepClone() => new ImageMetadata(this);
public ImageMetadata DeepClone() => new(this);

/// <summary>
/// Synchronizes the profiles with the current metadata.
Expand Down
41 changes: 41 additions & 0 deletions tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -259,5 +259,46 @@ public void Issue1962<TPixel>(TestImageProvider<TPixel> provider)

image.CompareFirstFrameToReferenceOutput(ImageComparer.Exact, provider);
}

// https://github.com/SixLabors/ImageSharp/issues/2012
[Theory]
[WithFile(TestImages.Gif.Issues.Issue2012EmptyXmp, PixelTypes.Rgba32)]
public void Issue2012EmptyXmp<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();

image.DebugSave(provider);
image.CompareFirstFrameToReferenceOutput(ImageComparer.Exact, provider);
}

// https://github.com/SixLabors/ImageSharp/issues/2012
[Theory]
[WithFile(TestImages.Gif.Issues.Issue2012BadMinCode, PixelTypes.Rgba32)]
public void Issue2012BadMinCode<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
Exception ex = Record.Exception(
() =>
{
using Image<TPixel> image = provider.GetImage();
image.DebugSave(provider);
});

Assert.NotNull(ex);
Assert.Contains("Gif Image does not contain a valid LZW minimum code.", ex.Message);
}

// https://bugzilla.mozilla.org/show_bug.cgi?id=55918
[Theory]
[WithFile(TestImages.Gif.Issues.DeferredClearCode, PixelTypes.Rgba32)]
public void IssueDeferredClearCode<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();

image.DebugSave(provider);
image.CompareFirstFrameToReferenceOutput(ImageComparer.Exact, provider);
}
}
}
3 changes: 2 additions & 1 deletion tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.

using System.IO;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
Expand Down Expand Up @@ -66,7 +67,7 @@ public void IgnoreMetadata_ControlsWhetherIccpIsParsed<TPixel>(TestImageProvider
[Theory]
[WithFile(TestImages.Webp.Lossy.WithXmp, PixelTypes.Rgba32, false)]
[WithFile(TestImages.Webp.Lossy.WithXmp, PixelTypes.Rgba32, true)]
public async void IgnoreMetadata_ControlsWhetherXmpIsParsed<TPixel>(TestImageProvider<TPixel> provider, bool ignoreMetadata)
public async Task IgnoreMetadata_ControlsWhetherXmpIsParsed<TPixel>(TestImageProvider<TPixel> provider, bool ignoreMetadata)
where TPixel : unmanaged, IPixel<TPixel>
{
var decoder = new WebpDecoder { IgnoreMetadata = ignoreMetadata };
Expand Down
Loading