Browse Source

Improve encoder perf

pull/527/head
James Jackson-South 8 years ago
parent
commit
1b86e7ef3a
  1. 23
      src/ImageSharp/Common/Helpers/DebugGuard.cs
  2. 25
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  3. 62
      src/ImageSharp/Formats/Gif/LzwEncoder.cs
  4. 60
      src/ImageSharp/Formats/Gif/PackedField.cs
  5. 12
      src/ImageSharp/Processing/Quantization/FrameQuantizers/PaletteFrameQuantizer{TPixel}.cs
  6. 25
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs

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

@ -4,6 +4,7 @@
using System;
using System.Diagnostics;
// TODO: These should just call the guard equivalents
namespace SixLabors.ImageSharp
{
/// <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>
/// Verifies, that the method parameter with specified target value is true
/// and throws an exception if it is found to be so.

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

@ -4,6 +4,8 @@
using System;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
@ -132,17 +134,19 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <returns>
/// The <see cref="int"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int GetTransparentIndex<TPixel>(QuantizedFrame<TPixel> quantized)
where TPixel : struct, IPixel<TPixel>
{
// Transparent pixels are much more likely to be found at the end of a palette
int index = -1;
var trans = default(Rgba32);
ref TPixel paletteRef = ref MemoryMarshal.GetReference(quantized.Palette.AsSpan());
for (int i = quantized.Palette.Length - 1; i >= 0; i--)
{
quantized.Palette[i].ToRgba32(ref trans);
if (trans.Equals(default(Rgba32)))
ref TPixel entry = ref Unsafe.Add(ref paletteRef, i);
entry.ToRgba32(ref trans);
if (trans.Equals(default))
{
index = i;
}
@ -155,6 +159,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Writes the file header signature and version to the stream.
/// </summary>
/// <param name="writer">The writer to write to the stream with.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteHeader(EndianBinaryWriter writer)
{
writer.Write(GifConstants.MagicNumber);
@ -336,15 +341,19 @@ namespace SixLabors.ImageSharp.Formats.Gif
var rgb = default(Rgb24);
using (IManagedByteBuffer colorTable = this.memoryManager.AllocateManagedByteBuffer(colorTableLength))
{
Span<byte> colorTableSpan = colorTable.Span;
// TODO: Pixel operations?
ref TPixel paletteRef = ref MemoryMarshal.GetReference(image.Palette.AsSpan());
ref byte colorTableRef = ref MemoryMarshal.GetReference(colorTable.Span);
for (int i = 0; i < pixelCount; i++)
{
int offset = i * 3;
image.Palette[i].ToRgb24(ref rgb);
colorTableSpan[offset] = rgb.R;
colorTableSpan[offset + 1] = rgb.G;
colorTableSpan[offset + 2] = rgb.B;
ref TPixel entry = ref Unsafe.Add(ref paletteRef, i);
entry.ToRgb24(ref rgb);
Unsafe.Add(ref colorTableRef, offset) = rgb.R;
Unsafe.Add(ref colorTableRef, offset + 1) = rgb.G;
Unsafe.Add(ref colorTableRef, offset + 2) = rgb.B;
}
writer.Write(colorTable.Array, 0, colorTableLength);

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

@ -2,9 +2,9 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Gif
@ -233,6 +233,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
/// <param name="bitCount">The number of bits</param>
/// <returns>See <see cref="int"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetMaxcode(int bitCount)
{
return (1 << bitCount) - 1;
@ -243,10 +244,12 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// flush the packet to disk.
/// </summary>
/// <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>
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)
{
this.FlushPacket(stream);
@ -257,6 +260,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Table clear for block compress
/// </summary>
/// <param name="stream">The output stream.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ClearBlock(Stream stream)
{
this.ResetCodeTable();
@ -269,15 +273,10 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <summary>
/// Reset the code table.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ResetCodeTable()
{
this.hashTable.Span.Fill(-1);
// Original code:
// for (int i = 0; i < size; ++i)
// {
// this.hashTable[i] = -1;
// }
}
/// <summary>
@ -309,6 +308,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
ent = this.NextPixel();
// TODO: PERF: It looks likt hshift could be calculated once statically.
hshift = 0;
for (fcode = this.hsize; fcode < 65536; fcode *= 2)
{
@ -323,22 +323,22 @@ namespace SixLabors.ImageSharp.Formats.Gif
this.Output(this.clearCode, stream);
Span<int> hashTableSpan = this.hashTable.Span;
Span<int> codeTableSpan = this.codeTable.Span;
ref int hashTableRef = ref MemoryMarshal.GetReference(this.hashTable.Span);
ref int codeTableRef = ref MemoryMarshal.GetReference(this.codeTable.Span);
while ((c = this.NextPixel()) != Eof)
{
fcode = (c << this.maxbits) + ent;
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;
}
// Non-empty slot
if (hashTableSpan[i] >= 0)
if (Unsafe.Add(ref hashTableRef, i) >= 0)
{
int disp = hsizeReg - i;
if (i == 0)
@ -353,15 +353,15 @@ namespace SixLabors.ImageSharp.Formats.Gif
i += hsizeReg;
}
if (hashTableSpan[i] == fcode)
if (Unsafe.Add(ref hashTableRef, i) == fcode)
{
ent = codeTableSpan[i];
ent = Unsafe.Add(ref codeTableRef, i);
break;
}
}
while (hashTableSpan[i] >= 0);
while (Unsafe.Add(ref hashTableRef, i) >= 0);
if (hashTableSpan[i] == fcode)
if (Unsafe.Add(ref hashTableRef, i) == fcode)
{
continue;
}
@ -371,8 +371,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
ent = c;
if (this.freeEntry < this.maxmaxcode)
{
codeTableSpan[i] = this.freeEntry++; // code -> hashtable
hashTableSpan[i] = fcode;
Unsafe.Add(ref codeTableRef, i) = this.freeEntry++; // code -> hashtable
Unsafe.Add(ref hashTableRef, i) = fcode;
}
else
{
@ -390,14 +390,12 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Flush the packet to disk, and reset the accumulator.
/// </summary>
/// <param name="outStream">The output stream.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void FlushPacket(Stream outStream)
{
if (this.accumulatorCount > 0)
{
outStream.WriteByte((byte)this.accumulatorCount);
outStream.Write(this.accumulators, 0, this.accumulatorCount);
this.accumulatorCount = 0;
}
outStream.WriteByte((byte)this.accumulatorCount);
outStream.Write(this.accumulators, 0, this.accumulatorCount);
this.accumulatorCount = 0;
}
/// <summary>
@ -424,6 +422,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <param name="outs">The stream to write to.</param>
private void Output(int code, Stream outs)
{
ref byte accumulatorsRef = ref MemoryMarshal.GetReference(this.accumulators.AsSpan());
this.currentAccumulator &= Masks[this.currentBits];
if (this.currentBits > 0)
@ -439,7 +438,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
while (this.currentBits >= 8)
{
this.AddCharacter((byte)(this.currentAccumulator & 0xff), outs);
this.AddCharacter((byte)(this.currentAccumulator & 0xFF), ref accumulatorsRef, outs);
this.currentAccumulator >>= 8;
this.currentBits -= 8;
}
@ -467,12 +466,15 @@ namespace SixLabors.ImageSharp.Formats.Gif
// At EOF, write the rest of the buffer.
while (this.currentBits > 0)
{
this.AddCharacter((byte)(this.currentAccumulator & 0xff), outs);
this.AddCharacter((byte)(this.currentAccumulator & 0xFF), ref accumulatorsRef, outs);
this.currentAccumulator >>= 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.
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Gif
{
@ -21,6 +23,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
public byte Byte
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
int returnValue = 0;
@ -53,7 +56,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <returns>The <see cref="PackedField"/></returns>
public static PackedField FromInt(byte value)
{
PackedField packed = default(PackedField);
PackedField packed = default;
packed.SetBits(0, 8, value);
return packed;
}
@ -70,12 +73,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </param>
public void SetBit(int index, bool valueToSet)
{
if (index < 0 || index > 7)
{
string message = $"Index must be between 0 and 7. Supplied index: {index}";
throw new ArgumentOutOfRangeException(nameof(index), message);
}
DebugGuard.MustBeBetweenOrEqualTo(index, 0, 7, nameof(index));
Bits[index] = valueToSet;
}
@ -88,18 +86,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <param name="valueToSet">The value to set the bits to.</param>
public void SetBits(int startIndex, int length, int valueToSet)
{
if (startIndex < 0 || startIndex > 7)
{
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);
}
DebugGuard.MustBeBetweenOrEqualTo(startIndex, 0, 7, nameof(startIndex));
DebugCheckLength(startIndex, length);
int bitShift = length - 1;
for (int i = startIndex; i < startIndex + length; i++)
@ -121,12 +109,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </returns>
public bool GetBit(int index)
{
if (index < 0 || index > 7)
{
string message = $"Index must be between 0 and 7. Supplied index: {index}";
throw new ArgumentOutOfRangeException(nameof(index), message);
}
DebugGuard.MustBeBetweenOrEqualTo(index, 0, 7, nameof(index));
return Bits[index];
}
@ -140,19 +123,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </returns>
public int GetBits(int startIndex, int length)
{
if (startIndex < 0 || startIndex > 7)
{
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);
}
DebugGuard.MustBeBetweenOrEqualTo(startIndex, 1, 8, nameof(startIndex));
DebugCheckLength(startIndex, length);
int returnValue = 0;
int bitShift = length - 1;
@ -189,5 +161,17 @@ namespace SixLabors.ImageSharp.Formats.Gif
{
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.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
@ -45,18 +46,18 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
TPixel sourcePixel = source[0, 0];
TPixel previousPixel = sourcePixel;
byte pixelValue = this.QuantizePixel(sourcePixel);
TPixel[] colorPalette = this.GetPalette();
TPixel transformedPixel = colorPalette[pixelValue];
ref TPixel colorPaletteRef = ref MemoryMarshal.GetReference(this.GetPalette().AsSpan());
TPixel transformedPixel = Unsafe.Add(ref colorPaletteRef, pixelValue);
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
for (int x = 0; x < width; x++)
{
// 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
// rather than calculating it again. This is an inexpensive optimization.
@ -70,7 +71,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
if (this.Dither)
{
transformedPixel = colorPalette[pixelValue];
transformedPixel = Unsafe.Add(ref colorPaletteRef, pixelValue);
}
}
@ -86,6 +87,7 @@ namespace SixLabors.ImageSharp.Processing.Quantization.FrameQuantizers
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override TPixel[] GetPalette()
{
return this.colors;

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

@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.MetaData;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Quantization;
using Xunit;
// ReSharper disable InconsistentNaming
@ -22,28 +23,28 @@ namespace SixLabors.ImageSharp.Tests
{
using (Image<TPixel> image = provider.GetImage())
{
provider.Utility.SaveTestOutputFile(image, "gif", new GifEncoder());
provider.Utility.SaveTestOutputFile(image, "gif", new GifEncoder() { Quantizer = new PaletteQuantizer() });
}
}
[Fact]
public void Encode_IgnoreMetadataIsFalse_CommentsAreWritten()
{
GifEncoder options = new GifEncoder()
var options = new GifEncoder()
{
IgnoreMetadata = false
};
TestFile testFile = TestFile.Create(TestImages.Gif.Rings);
var testFile = TestFile.Create(TestImages.Gif.Rings);
using (Image<Rgba32> input = testFile.CreateImage())
{
using (MemoryStream memStream = new MemoryStream())
using (var memStream = new MemoryStream())
{
input.Save(memStream, options);
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("Comments", output.MetaData.Properties[0].Name);
@ -56,21 +57,21 @@ namespace SixLabors.ImageSharp.Tests
[Fact]
public void Encode_IgnoreMetadataIsTrue_CommentsAreNotWritten()
{
GifEncoder options = new GifEncoder()
var options = new GifEncoder()
{
IgnoreMetadata = true
};
TestFile testFile = TestFile.Create(TestImages.Gif.Rings);
var testFile = TestFile.Create(TestImages.Gif.Rings);
using (Image<Rgba32> input = testFile.CreateImage())
{
using (MemoryStream memStream = new MemoryStream())
using (var memStream = new MemoryStream())
{
input.SaveAsGif(memStream, options);
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);
}
@ -81,17 +82,17 @@ namespace SixLabors.ImageSharp.Tests
[Fact]
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);
input.MetaData.Properties.Add(new ImageProperty("Comments", comments));
using (MemoryStream memStream = new MemoryStream())
using (var memStream = new MemoryStream())
{
input.Save(memStream, new GifEncoder());
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("Comments", output.MetaData.Properties[0].Name);

Loading…
Cancel
Save