Browse Source

Merge branch 'master' into js/faster-pdf-js-decoder

pull/525/head
James Jackson-South 8 years ago
parent
commit
97f76253f1
  1. 23
      src/ImageSharp/Common/Helpers/DebugGuard.cs
  2. 14
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  3. 40
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  4. 37
      src/ImageSharp/Formats/Gif/LzwDecoder.cs
  5. 62
      src/ImageSharp/Formats/Gif/LzwEncoder.cs
  6. 60
      src/ImageSharp/Formats/Gif/PackedField.cs
  7. 12
      src/ImageSharp/Processing/Quantization/FrameQuantizers/PaletteFrameQuantizer{TPixel}.cs
  8. 43
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  9. 37
      tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs

23
src/ImageSharp/Common/Helpers/DebugGuard.cs

@ -4,6 +4,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
// TODO: These should just call the guard equivalents
namespace SixLabors.ImageSharp namespace SixLabors.ImageSharp
{ {
/// <summary> /// <summary>
@ -114,6 +115,28 @@ namespace SixLabors.ImageSharp
} }
} }
/// <summary>
/// Verifies that the specified value is greater than or equal to a minimum value and less than
/// or equal to a maximum value and throws an exception if it is not.
/// </summary>
/// <param name="value">The target value, which should be validated.</param>
/// <param name="min">The minimum value.</param>
/// <param name="max">The maximum value.</param>
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
/// <typeparam name="TValue">The type of the value.</typeparam>
/// <exception cref="ArgumentException">
/// <paramref name="value"/> is less than the minimum value of greater than the maximum value.
/// </exception>
[Conditional("DEBUG")]
public static void MustBeBetweenOrEqualTo<TValue>(TValue value, TValue min, TValue max, string parameterName)
where TValue : IComparable<TValue>
{
if (value.CompareTo(min) < 0 || value.CompareTo(max) > 0)
{
throw new ArgumentOutOfRangeException(parameterName, $"Value {value} must be greater than or equal to {min} and less than or equal to {max}.");
}
}
/// <summary> /// <summary>
/// Verifies, that the method parameter with specified target value is true /// Verifies, that the method parameter with specified target value is true
/// and throws an exception if it is found to be so. /// and throws an exception if it is found to be so.

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

@ -5,6 +5,7 @@ using System;
using System.Buffers; using System.Buffers;
using System.IO; using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text; using System.Text;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
@ -410,13 +411,12 @@ namespace SixLabors.ImageSharp.Formats.Gif
private void ReadFrameColors<TPixel>(ref Image<TPixel> image, ref ImageFrame<TPixel> previousFrame, Span<byte> indices, Span<byte> colorTable, GifImageDescriptor descriptor) private void ReadFrameColors<TPixel>(ref Image<TPixel> image, ref ImageFrame<TPixel> previousFrame, Span<byte> indices, Span<byte> colorTable, GifImageDescriptor descriptor)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
ref byte indicesRef = ref MemoryMarshal.GetReference(indices);
int imageWidth = this.logicalScreenDescriptor.Width; int imageWidth = this.logicalScreenDescriptor.Width;
int imageHeight = this.logicalScreenDescriptor.Height; int imageHeight = this.logicalScreenDescriptor.Height;
ImageFrame<TPixel> prevFrame = null; ImageFrame<TPixel> prevFrame = null;
ImageFrame<TPixel> currentFrame = null; ImageFrame<TPixel> currentFrame = null;
ImageFrame<TPixel> imageFrame; ImageFrame<TPixel> imageFrame;
if (previousFrame == null) if (previousFrame == null)
@ -479,7 +479,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
} }
writeY = interlaceY + descriptor.Top; writeY = interlaceY + descriptor.Top;
interlaceY += interlaceIncrement; interlaceY += interlaceIncrement;
} }
else else
@ -487,14 +486,13 @@ namespace SixLabors.ImageSharp.Formats.Gif
writeY = y; writeY = y;
} }
Span<TPixel> rowSpan = imageFrame.GetPixelRowSpan(writeY); ref TPixel rowRef = ref MemoryMarshal.GetReference(imageFrame.GetPixelRowSpan(writeY));
var rgba = new Rgba32(0, 0, 0, 255); var rgba = new Rgba32(0, 0, 0, 255);
// #403 The left + width value can be larger than the image width // #403 The left + width value can be larger than the image width
for (int x = descriptor.Left; x < descriptor.Left + descriptor.Width && x < rowSpan.Length; x++) for (int x = descriptor.Left; x < descriptor.Left + descriptor.Width && x < imageWidth; x++)
{ {
int index = indices[i]; int index = Unsafe.Add(ref indicesRef, i);
if (this.graphicsControlExtension == null || if (this.graphicsControlExtension == null ||
this.graphicsControlExtension.TransparencyFlag == false || this.graphicsControlExtension.TransparencyFlag == false ||
@ -502,7 +500,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
{ {
int indexOffset = index * 3; int indexOffset = index * 3;
ref TPixel pixel = ref rowSpan[x]; ref TPixel pixel = ref Unsafe.Add(ref rowRef, x);
rgba.Rgb = colorTable.GetRgb24(indexOffset); rgba.Rgb = colorTable.GetRgb24(indexOffset);
pixel.PackFromRgba32(rgba); pixel.PackFromRgba32(rgba);

40
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -4,6 +4,8 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text; using System.Text;
using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
@ -11,6 +13,9 @@ using SixLabors.ImageSharp.MetaData;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Quantization; using SixLabors.ImageSharp.Processing.Quantization;
// TODO: This is causing more GC collections than I'm happy with.
// This is likely due to the number of short writes to the stream we are doing.
// We should investigate reducing them since we know the length of the byte array we require for multiple parts.
namespace SixLabors.ImageSharp.Formats.Gif namespace SixLabors.ImageSharp.Formats.Gif
{ {
/// <summary> /// <summary>
@ -45,11 +50,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
private int bitDepth; private int bitDepth;
/// <summary>
/// Whether the current image has multiple frames.
/// </summary>
private bool hasFrames;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class. /// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
/// </summary> /// </summary>
@ -78,8 +78,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
// Do not use IDisposable pattern here as we want to preserve the stream. // Do not use IDisposable pattern here as we want to preserve the stream.
var writer = new EndianBinaryWriter(Endianness.LittleEndian, stream); var writer = new EndianBinaryWriter(Endianness.LittleEndian, stream);
this.hasFrames = image.Frames.Count > 1;
// Quantize the image returning a palette. // Quantize the image returning a palette.
QuantizedFrame<TPixel> quantized = this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(image.Frames.RootFrame); QuantizedFrame<TPixel> quantized = this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(image.Frames.RootFrame);
@ -98,9 +96,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
this.WriteComments(image, writer); this.WriteComments(image, writer);
// Write additional frames. // Write additional frames.
if (this.hasFrames) if (image.Frames.Count > 1)
{ {
this.WriteApplicationExtension(writer, image.MetaData.RepeatCount, image.Frames.Count); this.WriteApplicationExtension(writer, image.MetaData.RepeatCount);
} }
foreach (ImageFrame<TPixel> frame in image.Frames) foreach (ImageFrame<TPixel> frame in image.Frames)
@ -138,11 +136,12 @@ namespace SixLabors.ImageSharp.Formats.Gif
// Transparent pixels are much more likely to be found at the end of a palette // Transparent pixels are much more likely to be found at the end of a palette
int index = -1; int index = -1;
var trans = default(Rgba32); var trans = default(Rgba32);
ref TPixel paletteRef = ref MemoryMarshal.GetReference(quantized.Palette.AsSpan());
for (int i = quantized.Palette.Length - 1; i >= 0; i--) for (int i = quantized.Palette.Length - 1; i >= 0; i--)
{ {
quantized.Palette[i].ToRgba32(ref trans); ref TPixel entry = ref Unsafe.Add(ref paletteRef, i);
entry.ToRgba32(ref trans);
if (trans.Equals(default(Rgba32))) if (trans.Equals(default))
{ {
index = i; index = i;
} }
@ -155,6 +154,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Writes the file header signature and version to the stream. /// Writes the file header signature and version to the stream.
/// </summary> /// </summary>
/// <param name="writer">The writer to write to the stream with.</param> /// <param name="writer">The writer to write to the stream with.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteHeader(EndianBinaryWriter writer) private void WriteHeader(EndianBinaryWriter writer)
{ {
writer.Write(GifConstants.MagicNumber); writer.Write(GifConstants.MagicNumber);
@ -201,11 +201,10 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
/// <param name="writer">The writer to write to the stream with.</param> /// <param name="writer">The writer to write to the stream with.</param>
/// <param name="repeatCount">The animated image repeat count.</param> /// <param name="repeatCount">The animated image repeat count.</param>
/// <param name="frames">The number of image frames.</param> private void WriteApplicationExtension(EndianBinaryWriter writer, ushort repeatCount)
private void WriteApplicationExtension(EndianBinaryWriter writer, ushort repeatCount, int frames)
{ {
// Application Extension Header // Application Extension Header
if (repeatCount != 1 && frames > 0) if (repeatCount != 1)
{ {
this.buffer[0] = GifConstants.ExtensionIntroducer; this.buffer[0] = GifConstants.ExtensionIntroducer;
this.buffer[1] = GifConstants.ApplicationExtensionLabel; this.buffer[1] = GifConstants.ApplicationExtensionLabel;
@ -336,15 +335,14 @@ namespace SixLabors.ImageSharp.Formats.Gif
var rgb = default(Rgb24); var rgb = default(Rgb24);
using (IManagedByteBuffer colorTable = this.memoryManager.AllocateManagedByteBuffer(colorTableLength)) using (IManagedByteBuffer colorTable = this.memoryManager.AllocateManagedByteBuffer(colorTableLength))
{ {
Span<byte> colorTableSpan = colorTable.Span; ref TPixel paletteRef = ref MemoryMarshal.GetReference(image.Palette.AsSpan());
ref Rgb24 rgb24Ref = ref Unsafe.As<byte, Rgb24>(ref MemoryMarshal.GetReference(colorTable.Span));
for (int i = 0; i < pixelCount; i++) for (int i = 0; i < pixelCount; i++)
{ {
int offset = i * 3; ref TPixel entry = ref Unsafe.Add(ref paletteRef, i);
image.Palette[i].ToRgb24(ref rgb); entry.ToRgb24(ref rgb);
colorTableSpan[offset] = rgb.R; Unsafe.Add(ref rgb24Ref, i) = rgb;
colorTableSpan[offset + 1] = rgb.G;
colorTableSpan[offset + 2] = rgb.B;
} }
writer.Write(colorTable.Array, 0, colorTableLength); writer.Write(colorTable.Array, 0, colorTableLength);

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

@ -2,9 +2,9 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Buffers;
using System.IO; using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Gif namespace SixLabors.ImageSharp.Formats.Gif
@ -115,14 +115,14 @@ namespace SixLabors.ImageSharp.Formats.Gif
int data = 0; int data = 0;
int first = 0; int first = 0;
Span<int> prefixSpan = this.prefix.Span; ref int prefixRef = ref MemoryMarshal.GetReference(this.prefix.Span);
Span<int> suffixSpan = this.suffix.Span; ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.Span);
Span<int> pixelStackSpan = this.pixelStack.Span; ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.Span);
ref byte pixelsRef = ref MemoryMarshal.GetReference(pixels);
for (code = 0; code < clearCode; code++) for (code = 0; code < clearCode; code++)
{ {
prefixSpan[code] = 0; Unsafe.Add(ref suffixRef, code) = (byte)code;
suffixSpan[code] = (byte)code;
} }
byte[] buffer = new byte[255]; byte[] buffer = new byte[255];
@ -176,7 +176,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
if (oldCode == NullCode) if (oldCode == NullCode)
{ {
pixelStackSpan[top++] = suffixSpan[code]; Unsafe.Add(ref pixelStackRef, top++) = Unsafe.Add(ref suffixRef, code);
oldCode = code; oldCode = code;
first = code; first = code;
continue; continue;
@ -185,27 +185,27 @@ namespace SixLabors.ImageSharp.Formats.Gif
int inCode = code; int inCode = code;
if (code == availableCode) if (code == availableCode)
{ {
pixelStackSpan[top++] = (byte)first; Unsafe.Add(ref pixelStackRef, top++) = (byte)first;
code = oldCode; code = oldCode;
} }
while (code > clearCode) while (code > clearCode)
{ {
pixelStackSpan[top++] = suffixSpan[code]; Unsafe.Add(ref pixelStackRef, top++) = Unsafe.Add(ref suffixRef, code);
code = prefixSpan[code]; code = Unsafe.Add(ref prefixRef, code);
} }
first = suffixSpan[code]; int suffixCode = Unsafe.Add(ref suffixRef, code);
first = suffixCode;
pixelStackSpan[top++] = suffixSpan[code]; Unsafe.Add(ref pixelStackRef, 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 (availableCode < MaxStackSize)
{ {
prefixSpan[availableCode] = oldCode; Unsafe.Add(ref prefixRef, availableCode) = oldCode;
suffixSpan[availableCode] = first; Unsafe.Add(ref suffixRef, availableCode) = first;
availableCode++; availableCode++;
if (availableCode == codeMask + 1 && availableCode < MaxStackSize) if (availableCode == codeMask + 1 && availableCode < MaxStackSize)
{ {
@ -221,7 +221,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
top--; top--;
// Clear missing pixels // Clear missing pixels
pixels[xyz++] = (byte)pixelStackSpan[top]; Unsafe.Add(ref pixelsRef, xyz++) = (byte)Unsafe.Add(ref pixelStackRef, top);
} }
} }
@ -238,8 +238,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
/// <param name="buffer">The buffer to store the block in.</param> /// <param name="buffer">The buffer to store the block in.</param>
/// <returns> /// <returns>
/// The <see cref="T:byte[]"/>. /// The <see cref="int"/>.
/// </returns> /// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int ReadBlock(byte[] buffer) private int ReadBlock(byte[] buffer)
{ {
int bufferSize = this.stream.ReadByte(); int bufferSize = this.stream.ReadByte();

62
src/ImageSharp/Formats/Gif/LzwEncoder.cs

@ -2,9 +2,9 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Buffers;
using System.IO; using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Gif namespace SixLabors.ImageSharp.Formats.Gif
@ -233,6 +233,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
/// <param name="bitCount">The number of bits</param> /// <param name="bitCount">The number of bits</param>
/// <returns>See <see cref="int"/></returns> /// <returns>See <see cref="int"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetMaxcode(int bitCount) private static int GetMaxcode(int bitCount)
{ {
return (1 << bitCount) - 1; return (1 << bitCount) - 1;
@ -243,10 +244,12 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// flush the packet to disk. /// flush the packet to disk.
/// </summary> /// </summary>
/// <param name="c">The character to add.</param> /// <param name="c">The character to add.</param>
/// <param name="accumulatorsRef">The reference to the storage for packat accumulators</param>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
private void AddCharacter(byte c, Stream stream) [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void AddCharacter(byte c, ref byte accumulatorsRef, Stream stream)
{ {
this.accumulators[this.accumulatorCount++] = c; Unsafe.Add(ref accumulatorsRef, this.accumulatorCount++) = c;
if (this.accumulatorCount >= 254) if (this.accumulatorCount >= 254)
{ {
this.FlushPacket(stream); this.FlushPacket(stream);
@ -257,6 +260,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Table clear for block compress /// Table clear for block compress
/// </summary> /// </summary>
/// <param name="stream">The output stream.</param> /// <param name="stream">The output stream.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ClearBlock(Stream stream) private void ClearBlock(Stream stream)
{ {
this.ResetCodeTable(); this.ResetCodeTable();
@ -269,15 +273,10 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <summary> /// <summary>
/// Reset the code table. /// Reset the code table.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ResetCodeTable() private void ResetCodeTable()
{ {
this.hashTable.Span.Fill(-1); this.hashTable.Span.Fill(-1);
// Original code:
// for (int i = 0; i < size; ++i)
// {
// this.hashTable[i] = -1;
// }
} }
/// <summary> /// <summary>
@ -309,6 +308,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
ent = this.NextPixel(); ent = this.NextPixel();
// TODO: PERF: It looks likt hshift could be calculated once statically.
hshift = 0; hshift = 0;
for (fcode = this.hsize; fcode < 65536; fcode *= 2) for (fcode = this.hsize; fcode < 65536; fcode *= 2)
{ {
@ -323,22 +323,22 @@ namespace SixLabors.ImageSharp.Formats.Gif
this.Output(this.clearCode, stream); this.Output(this.clearCode, stream);
Span<int> hashTableSpan = this.hashTable.Span; ref int hashTableRef = ref MemoryMarshal.GetReference(this.hashTable.Span);
Span<int> codeTableSpan = this.codeTable.Span; ref int codeTableRef = ref MemoryMarshal.GetReference(this.codeTable.Span);
while ((c = this.NextPixel()) != Eof) while ((c = this.NextPixel()) != Eof)
{ {
fcode = (c << this.maxbits) + ent; fcode = (c << this.maxbits) + ent;
int i = (c << hshift) ^ ent /* = 0 */; int i = (c << hshift) ^ ent /* = 0 */;
if (hashTableSpan[i] == fcode) if (Unsafe.Add(ref hashTableRef, i) == fcode)
{ {
ent = codeTableSpan[i]; ent = Unsafe.Add(ref codeTableRef, i);
continue; continue;
} }
// Non-empty slot // Non-empty slot
if (hashTableSpan[i] >= 0) if (Unsafe.Add(ref hashTableRef, i) >= 0)
{ {
int disp = hsizeReg - i; int disp = hsizeReg - i;
if (i == 0) if (i == 0)
@ -353,15 +353,15 @@ namespace SixLabors.ImageSharp.Formats.Gif
i += hsizeReg; i += hsizeReg;
} }
if (hashTableSpan[i] == fcode) if (Unsafe.Add(ref hashTableRef, i) == fcode)
{ {
ent = codeTableSpan[i]; ent = Unsafe.Add(ref codeTableRef, i);
break; break;
} }
} }
while (hashTableSpan[i] >= 0); while (Unsafe.Add(ref hashTableRef, i) >= 0);
if (hashTableSpan[i] == fcode) if (Unsafe.Add(ref hashTableRef, i) == fcode)
{ {
continue; continue;
} }
@ -371,8 +371,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
ent = c; ent = c;
if (this.freeEntry < this.maxmaxcode) if (this.freeEntry < this.maxmaxcode)
{ {
codeTableSpan[i] = this.freeEntry++; // code -> hashtable Unsafe.Add(ref codeTableRef, i) = this.freeEntry++; // code -> hashtable
hashTableSpan[i] = fcode; Unsafe.Add(ref hashTableRef, i) = fcode;
} }
else else
{ {
@ -390,14 +390,12 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Flush the packet to disk, and reset the accumulator. /// Flush the packet to disk, and reset the accumulator.
/// </summary> /// </summary>
/// <param name="outStream">The output stream.</param> /// <param name="outStream">The output stream.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void FlushPacket(Stream outStream) private void FlushPacket(Stream outStream)
{ {
if (this.accumulatorCount > 0) outStream.WriteByte((byte)this.accumulatorCount);
{ outStream.Write(this.accumulators, 0, this.accumulatorCount);
outStream.WriteByte((byte)this.accumulatorCount); this.accumulatorCount = 0;
outStream.Write(this.accumulators, 0, this.accumulatorCount);
this.accumulatorCount = 0;
}
} }
/// <summary> /// <summary>
@ -424,6 +422,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <param name="outs">The stream to write to.</param> /// <param name="outs">The stream to write to.</param>
private void Output(int code, Stream outs) private void Output(int code, Stream outs)
{ {
ref byte accumulatorsRef = ref MemoryMarshal.GetReference(this.accumulators.AsSpan());
this.currentAccumulator &= Masks[this.currentBits]; this.currentAccumulator &= Masks[this.currentBits];
if (this.currentBits > 0) if (this.currentBits > 0)
@ -439,7 +438,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
while (this.currentBits >= 8) while (this.currentBits >= 8)
{ {
this.AddCharacter((byte)(this.currentAccumulator & 0xff), outs); this.AddCharacter((byte)(this.currentAccumulator & 0xFF), ref accumulatorsRef, outs);
this.currentAccumulator >>= 8; this.currentAccumulator >>= 8;
this.currentBits -= 8; this.currentBits -= 8;
} }
@ -467,12 +466,15 @@ namespace SixLabors.ImageSharp.Formats.Gif
// At EOF, write the rest of the buffer. // At EOF, write the rest of the buffer.
while (this.currentBits > 0) while (this.currentBits > 0)
{ {
this.AddCharacter((byte)(this.currentAccumulator & 0xff), outs); this.AddCharacter((byte)(this.currentAccumulator & 0xFF), ref accumulatorsRef, outs);
this.currentAccumulator >>= 8; this.currentAccumulator >>= 8;
this.currentBits -= 8; this.currentBits -= 8;
} }
this.FlushPacket(outs); if (this.accumulatorCount > 0)
{
this.FlushPacket(outs);
}
} }
} }

60
src/ImageSharp/Formats/Gif/PackedField.cs

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Gif namespace SixLabors.ImageSharp.Formats.Gif
{ {
@ -21,6 +23,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
public byte Byte public byte Byte
{ {
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get get
{ {
int returnValue = 0; int returnValue = 0;
@ -53,7 +56,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <returns>The <see cref="PackedField"/></returns> /// <returns>The <see cref="PackedField"/></returns>
public static PackedField FromInt(byte value) public static PackedField FromInt(byte value)
{ {
PackedField packed = default(PackedField); PackedField packed = default;
packed.SetBits(0, 8, value); packed.SetBits(0, 8, value);
return packed; return packed;
} }
@ -70,12 +73,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </param> /// </param>
public void SetBit(int index, bool valueToSet) public void SetBit(int index, bool valueToSet)
{ {
if (index < 0 || index > 7) DebugGuard.MustBeBetweenOrEqualTo(index, 0, 7, nameof(index));
{
string message = $"Index must be between 0 and 7. Supplied index: {index}";
throw new ArgumentOutOfRangeException(nameof(index), message);
}
Bits[index] = valueToSet; Bits[index] = valueToSet;
} }
@ -88,18 +86,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <param name="valueToSet">The value to set the bits to.</param> /// <param name="valueToSet">The value to set the bits to.</param>
public void SetBits(int startIndex, int length, int valueToSet) public void SetBits(int startIndex, int length, int valueToSet)
{ {
if (startIndex < 0 || startIndex > 7) DebugGuard.MustBeBetweenOrEqualTo(startIndex, 0, 7, nameof(startIndex));
{ DebugCheckLength(startIndex, length);
string message = $"Start index must be between 0 and 7. Supplied index: {startIndex}";
throw new ArgumentOutOfRangeException(nameof(startIndex), message);
}
if (length < 1 || startIndex + length > 8)
{
string message = "Length must be greater than zero and the sum of length and start index must be less than 8. "
+ $"Supplied length: {length}. Supplied start index: {startIndex}";
throw new ArgumentOutOfRangeException(nameof(length), message);
}
int bitShift = length - 1; int bitShift = length - 1;
for (int i = startIndex; i < startIndex + length; i++) for (int i = startIndex; i < startIndex + length; i++)
@ -121,12 +109,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </returns> /// </returns>
public bool GetBit(int index) public bool GetBit(int index)
{ {
if (index < 0 || index > 7) DebugGuard.MustBeBetweenOrEqualTo(index, 0, 7, nameof(index));
{
string message = $"Index must be between 0 and 7. Supplied index: {index}";
throw new ArgumentOutOfRangeException(nameof(index), message);
}
return Bits[index]; return Bits[index];
} }
@ -140,19 +123,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </returns> /// </returns>
public int GetBits(int startIndex, int length) public int GetBits(int startIndex, int length)
{ {
if (startIndex < 0 || startIndex > 7) DebugGuard.MustBeBetweenOrEqualTo(startIndex, 1, 8, nameof(startIndex));
{ DebugCheckLength(startIndex, length);
string message = $"Start index must be between 0 and 7. Supplied index: {startIndex}";
throw new ArgumentOutOfRangeException(nameof(startIndex), message);
}
if (length < 1 || startIndex + length > 8)
{
string message = "Length must be greater than zero and the sum of length and start index must be less than 8. "
+ $"Supplied length: {length}. Supplied start index: {startIndex}";
throw new ArgumentOutOfRangeException(nameof(length), message);
}
int returnValue = 0; int returnValue = 0;
int bitShift = length - 1; int bitShift = length - 1;
@ -189,5 +161,17 @@ namespace SixLabors.ImageSharp.Formats.Gif
{ {
return this.Byte.GetHashCode(); return this.Byte.GetHashCode();
} }
[Conditional("DEBUG")]
private static void DebugCheckLength(int startIndex, int length)
{
if (length < 1 || startIndex + length > 8)
{
string message = "Length must be greater than zero and the sum of length and start index must be less than 8. "
+ $"Supplied length: {length}. Supplied start index: {startIndex}";
throw new ArgumentOutOfRangeException(nameof(length), message);
}
}
} }
} }

12
src/ImageSharp/Processing/Quantization/FrameQuantizers/PaletteFrameQuantizer{TPixel}.cs

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -45,18 +46,18 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
TPixel sourcePixel = source[0, 0]; TPixel sourcePixel = source[0, 0];
TPixel previousPixel = sourcePixel; TPixel previousPixel = sourcePixel;
byte pixelValue = this.QuantizePixel(sourcePixel); byte pixelValue = this.QuantizePixel(sourcePixel);
TPixel[] colorPalette = this.GetPalette(); ref TPixel colorPaletteRef = ref MemoryMarshal.GetReference(this.GetPalette().AsSpan());
TPixel transformedPixel = colorPalette[pixelValue]; TPixel transformedPixel = Unsafe.Add(ref colorPaletteRef, pixelValue);
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
Span<TPixel> row = source.GetPixelRowSpan(y); ref TPixel rowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y));
// And loop through each column // And loop through each column
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
{ {
// Get the pixel. // Get the pixel.
sourcePixel = row[x]; sourcePixel = Unsafe.Add(ref rowRef, x);
// Check if this is the same as the last pixel. If so use that value // Check if this is the same as the last pixel. If so use that value
// rather than calculating it again. This is an inexpensive optimization. // rather than calculating it again. This is an inexpensive optimization.
@ -70,7 +71,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
if (this.Dither) if (this.Dither)
{ {
transformedPixel = colorPalette[pixelValue]; transformedPixel = Unsafe.Add(ref colorPaletteRef, pixelValue);
} }
} }
@ -86,6 +87,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
} }
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override TPixel[] GetPalette() protected override TPixel[] GetPalette()
{ {
return this.colors; return this.colors;

43
tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs

@ -2,10 +2,11 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System.IO; using System.IO;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.MetaData; using SixLabors.ImageSharp.MetaData;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Quantization;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using Xunit; using Xunit;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
@ -14,6 +15,7 @@ namespace SixLabors.ImageSharp.Tests
public class GifEncoderTests public class GifEncoderTests
{ {
private const PixelTypes TestPixelTypes = PixelTypes.Rgba32 | PixelTypes.RgbaVector | PixelTypes.Argb32; private const PixelTypes TestPixelTypes = PixelTypes.Rgba32 | PixelTypes.RgbaVector | PixelTypes.Argb32;
private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.001F);
[Theory] [Theory]
[WithTestPatternImages(100, 100, TestPixelTypes)] [WithTestPatternImages(100, 100, TestPixelTypes)]
@ -22,28 +24,43 @@ namespace SixLabors.ImageSharp.Tests
{ {
using (Image<TPixel> image = provider.GetImage()) using (Image<TPixel> image = provider.GetImage())
{ {
provider.Utility.SaveTestOutputFile(image, "gif", new GifEncoder()); var encoder = new GifEncoder()
{
// Use the palette quantizer without dithering to ensure results
// are consistant
Quantizer = new PaletteQuantizer(false)
};
// Always save as we need to compare the encoded output.
provider.Utility.SaveTestOutputFile(image, "gif", encoder);
}
// Compare encoded result
string path = provider.Utility.GetTestOutputFileName("gif", null, true);
using (var encoded = Image.Load(path))
{
encoded.CompareToReferenceOutput(ValidatorComparer, provider, null, "gif");
} }
} }
[Fact] [Fact]
public void Encode_IgnoreMetadataIsFalse_CommentsAreWritten() public void Encode_IgnoreMetadataIsFalse_CommentsAreWritten()
{ {
GifEncoder options = new GifEncoder() var options = new GifEncoder()
{ {
IgnoreMetadata = false IgnoreMetadata = false
}; };
TestFile testFile = TestFile.Create(TestImages.Gif.Rings); var testFile = TestFile.Create(TestImages.Gif.Rings);
using (Image<Rgba32> input = testFile.CreateImage()) using (Image<Rgba32> input = testFile.CreateImage())
{ {
using (MemoryStream memStream = new MemoryStream()) using (var memStream = new MemoryStream())
{ {
input.Save(memStream, options); input.Save(memStream, options);
memStream.Position = 0; memStream.Position = 0;
using (Image<Rgba32> output = Image.Load<Rgba32>(memStream)) using (var output = Image.Load<Rgba32>(memStream))
{ {
Assert.Equal(1, output.MetaData.Properties.Count); Assert.Equal(1, output.MetaData.Properties.Count);
Assert.Equal("Comments", output.MetaData.Properties[0].Name); Assert.Equal("Comments", output.MetaData.Properties[0].Name);
@ -56,21 +73,21 @@ namespace SixLabors.ImageSharp.Tests
[Fact] [Fact]
public void Encode_IgnoreMetadataIsTrue_CommentsAreNotWritten() public void Encode_IgnoreMetadataIsTrue_CommentsAreNotWritten()
{ {
GifEncoder options = new GifEncoder() var options = new GifEncoder()
{ {
IgnoreMetadata = true IgnoreMetadata = true
}; };
TestFile testFile = TestFile.Create(TestImages.Gif.Rings); var testFile = TestFile.Create(TestImages.Gif.Rings);
using (Image<Rgba32> input = testFile.CreateImage()) using (Image<Rgba32> input = testFile.CreateImage())
{ {
using (MemoryStream memStream = new MemoryStream()) using (var memStream = new MemoryStream())
{ {
input.SaveAsGif(memStream, options); input.SaveAsGif(memStream, options);
memStream.Position = 0; memStream.Position = 0;
using (Image<Rgba32> output = Image.Load<Rgba32>(memStream)) using (var output = Image.Load<Rgba32>(memStream))
{ {
Assert.Equal(0, output.MetaData.Properties.Count); Assert.Equal(0, output.MetaData.Properties.Count);
} }
@ -81,17 +98,17 @@ namespace SixLabors.ImageSharp.Tests
[Fact] [Fact]
public void Encode_WhenCommentIsTooLong_CommentIsTrimmed() public void Encode_WhenCommentIsTooLong_CommentIsTrimmed()
{ {
using (Image<Rgba32> input = new Image<Rgba32>(1, 1)) using (var input = new Image<Rgba32>(1, 1))
{ {
string comments = new string('c', 256); string comments = new string('c', 256);
input.MetaData.Properties.Add(new ImageProperty("Comments", comments)); input.MetaData.Properties.Add(new ImageProperty("Comments", comments));
using (MemoryStream memStream = new MemoryStream()) using (var memStream = new MemoryStream())
{ {
input.Save(memStream, new GifEncoder()); input.Save(memStream, new GifEncoder());
memStream.Position = 0; memStream.Position = 0;
using (Image<Rgba32> output = Image.Load<Rgba32>(memStream)) using (var output = Image.Load<Rgba32>(memStream))
{ {
Assert.Equal(1, output.MetaData.Properties.Count); Assert.Equal(1, output.MetaData.Properties.Count);
Assert.Equal("Comments", output.MetaData.Properties[0].Name); Assert.Equal("Comments", output.MetaData.Properties[0].Name);

37
tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs

@ -87,6 +87,37 @@ namespace SixLabors.ImageSharp.Tests
return image; return image;
} }
/// <summary>
/// Saves the image only when not running in the CI server.
/// </summary>
/// <typeparam name="TPixel">The pixel format</typeparam>
/// <param name="image">The image</param>
/// <param name="provider">The image provider</param>
/// <param name="encoder">The image encoder</param>
/// <param name="testOutputDetails">Details to be concatenated to the test output file, describing the parameters of the test.</param>
/// <param name="appendPixelTypeToFileName">A boolean indicating whether to append the pixel type to the output file name.</param>
public static Image<TPixel> DebugSave<TPixel>(
this Image<TPixel> image,
ITestImageProvider provider,
IImageEncoder encoder,
object testOutputDetails = null,
bool appendPixelTypeToFileName = true)
where TPixel : struct, IPixel<TPixel>
{
if (TestEnvironment.RunsOnCI)
{
return image;
}
// We are running locally then we want to save it out
provider.Utility.SaveTestOutputFile(
image,
encoder: encoder,
testOutputDetails: testOutputDetails,
appendPixelTypeToFileName: appendPixelTypeToFileName);
return image;
}
public static Image<TPixel> DebugSaveMultiFrame<TPixel>( public static Image<TPixel> DebugSaveMultiFrame<TPixel>(
this Image<TPixel> image, this Image<TPixel> image,
ITestImageProvider provider, ITestImageProvider provider,
@ -168,7 +199,7 @@ namespace SixLabors.ImageSharp.Tests
provider, provider,
testOutputDetails, testOutputDetails,
extension, extension,
appendPixelTypeToFileName)) appendPixelTypeToFileName))
{ {
comparer.VerifySimilarity(referenceImage, image); comparer.VerifySimilarity(referenceImage, image);
} }
@ -272,7 +303,7 @@ namespace SixLabors.ImageSharp.Tests
} }
Image<TPixel> firstTemp = temporaryFrameImages[0]; Image<TPixel> firstTemp = temporaryFrameImages[0];
var result = new Image<TPixel>(firstTemp.Width, firstTemp.Height); var result = new Image<TPixel>(firstTemp.Width, firstTemp.Height);
foreach (Image<TPixel> fi in temporaryFrameImages) foreach (Image<TPixel> fi in temporaryFrameImages)
@ -345,7 +376,7 @@ namespace SixLabors.ImageSharp.Tests
{ {
return CompareToOriginal(image, provider, ImageComparer.Tolerant()); return CompareToOriginal(image, provider, ImageComparer.Tolerant());
} }
public static Image<TPixel> CompareToOriginal<TPixel>( public static Image<TPixel> CompareToOriginal<TPixel>(
this Image<TPixel> image, this Image<TPixel> image,
ITestImageProvider provider, ITestImageProvider provider,

Loading…
Cancel
Save