Browse Source

Optimize LZW Encoder

af/merge-core
James Jackson-South 10 years ago
parent
commit
5cf4c6db28
  1. 102
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  2. 156
      src/ImageSharp/Formats/Gif/LzwEncoder.cs
  3. 11
      src/ImageSharp/Quantizers/Octree/OctreeQuantizer.cs
  4. 3
      src/ImageSharp/Quantizers/QuantizedImage.cs

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

@ -6,11 +6,13 @@
namespace ImageSharp.Formats namespace ImageSharp.Formats
{ {
using System; using System;
using System.Buffers;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using IO; using IO;
using Quantizers; using Quantizers;
/// <summary> /// <summary>
@ -18,6 +20,11 @@ namespace ImageSharp.Formats
/// </summary> /// </summary>
internal sealed class GifEncoderCore internal sealed class GifEncoderCore
{ {
/// <summary>
/// The pixel buffer, used to reduce allocations.
/// </summary>
private readonly byte[] pixelBuffer = new byte[3];
/// <summary> /// <summary>
/// The number of bits requires to store the image palette. /// The number of bits requires to store the image palette.
/// </summary> /// </summary>
@ -47,8 +54,7 @@ namespace ImageSharp.Formats
/// <param name="image">The <see cref="Image{TColor, TPacked}"/> to encode from.</param> /// <param name="image">The <see cref="Image{TColor, TPacked}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param> /// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
public void Encode<TColor, TPacked>(Image<TColor, TPacked> image, Stream stream) public void Encode<TColor, TPacked>(Image<TColor, TPacked> image, Stream stream)
where TColor : struct, IPackedPixel<TPacked> where TColor : struct, IPackedPixel<TPacked> where TPacked : struct
where TPacked : struct
{ {
Guard.NotNull(image, nameof(image)); Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
@ -116,11 +122,10 @@ namespace ImageSharp.Formats
/// The <see cref="int"/>. /// The <see cref="int"/>.
/// </returns> /// </returns>
private static int GetTransparentIndex<TColor, TPacked>(QuantizedImage<TColor, TPacked> quantized) private static int GetTransparentIndex<TColor, TPacked>(QuantizedImage<TColor, TPacked> quantized)
where TColor : struct, IPackedPixel<TPacked> where TColor : struct, IPackedPixel<TPacked> where TPacked : struct
where TPacked : struct
{ {
// Find the lowest alpha value and make it the transparent index. // Find the lowest alpha value and make it the transparent index.
int index = 255; int index = -1;
float alpha = 1; float alpha = 1;
for (int i = 0; i < quantized.Palette.Length; i++) for (int i = 0; i < quantized.Palette.Length; i++)
{ {
@ -152,9 +157,10 @@ namespace ImageSharp.Formats
/// <param name="image">The image to encode.</param> /// <param name="image">The image to encode.</param>
/// <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="tranparencyIndex">The transparency index to set the default background index to.</param> /// <param name="tranparencyIndex">The transparency index to set the default background index to.</param>
private void WriteLogicalScreenDescriptor<TColor, TPacked>(Image<TColor, TPacked> image, EndianBinaryWriter writer, int tranparencyIndex) private void WriteLogicalScreenDescriptor<TColor, TPacked>(
where TColor : struct, IPackedPixel<TPacked> Image<TColor, TPacked> image,
where TPacked : struct EndianBinaryWriter writer,
int tranparencyIndex) where TColor : struct, IPackedPixel<TPacked> where TPacked : struct
{ {
GifLogicalScreenDescriptor descriptor = new GifLogicalScreenDescriptor GifLogicalScreenDescriptor descriptor = new GifLogicalScreenDescriptor
{ {
@ -169,18 +175,17 @@ namespace ImageSharp.Formats
writer.Write((ushort)descriptor.Height); writer.Write((ushort)descriptor.Height);
PackedField field = default(PackedField); PackedField field = default(PackedField);
field.SetBit(0, descriptor.GlobalColorTableFlag); // 1 : Global color table flag = 1 || 0 (GCT used/ not used) field.SetBit(0, descriptor.GlobalColorTableFlag); // 1 : Global color table flag = 1 || 0 (GCT used/ not used)
field.SetBits(1, 3, descriptor.GlobalColorTableSize); // 2-4 : color resolution field.SetBits(1, 3, descriptor.GlobalColorTableSize); // 2-4 : color resolution
field.SetBit(4, false); // 5 : GCT sort flag = 0 field.SetBit(4, false); // 5 : GCT sort flag = 0
field.SetBits(5, 3, descriptor.GlobalColorTableSize); // 6-8 : GCT size. 2^(N+1) field.SetBits(5, 3, descriptor.GlobalColorTableSize); // 6-8 : GCT size. 2^(N+1)
// Reduce the number of writes // Reduce the number of writes
byte[] arr = byte[] arr =
{ {
field.Byte, field.Byte, descriptor.BackgroundColorIndex, // Background Color Index
descriptor.BackgroundColorIndex, // Background Color Index descriptor.PixelAspectRatio // Pixel aspect ratio. Assume 1:1
descriptor.PixelAspectRatio // Pixel aspect ratio. Assume 1:1 };
};
writer.Write(arr); writer.Write(arr);
} }
@ -197,11 +202,10 @@ namespace ImageSharp.Formats
if (repeatCount != 1 && frames > 0) if (repeatCount != 1 && frames > 0)
{ {
byte[] ext = byte[] ext =
{ {
GifConstants.ExtensionIntroducer, GifConstants.ExtensionIntroducer, GifConstants.ApplicationExtensionLabel,
GifConstants.ApplicationExtensionLabel, GifConstants.ApplicationBlockSize
GifConstants.ApplicationBlockSize };
};
writer.Write(ext); writer.Write(ext);
@ -226,15 +230,16 @@ namespace ImageSharp.Formats
/// <param name="image">The <see cref="ImageBase{TColor, TPacked}"/> to encode.</param> /// <param name="image">The <see cref="ImageBase{TColor, TPacked}"/> to encode.</param>
/// <param name="writer">The stream to write to.</param> /// <param name="writer">The stream to write to.</param>
/// <param name="transparencyIndex">The index of the color in the color palette to make transparent.</param> /// <param name="transparencyIndex">The index of the color in the color palette to make transparent.</param>
private void WriteGraphicalControlExtension<TColor, TPacked>(ImageBase<TColor, TPacked> image, EndianBinaryWriter writer, int transparencyIndex) private void WriteGraphicalControlExtension<TColor, TPacked>(
where TColor : struct, IPackedPixel<TPacked> ImageBase<TColor, TPacked> image,
where TPacked : struct EndianBinaryWriter writer,
int transparencyIndex) where TColor : struct, IPackedPixel<TPacked> where TPacked : struct
{ {
// TODO: Check transparency logic. // TODO: Check transparency logic.
bool hasTransparent = transparencyIndex > -1; bool hasTransparent = transparencyIndex > -1;
DisposalMethod disposalMethod = hasTransparent DisposalMethod disposalMethod = hasTransparent
? DisposalMethod.RestoreToBackground ? DisposalMethod.RestoreToBackground
: DisposalMethod.Unspecified; : DisposalMethod.Unspecified;
GifGraphicsControlExtension extension = new GifGraphicsControlExtension() GifGraphicsControlExtension extension = new GifGraphicsControlExtension()
{ {
@ -247,10 +252,8 @@ namespace ImageSharp.Formats
// Reduce the number of writes. // Reduce the number of writes.
byte[] intro = byte[] intro =
{ {
GifConstants.ExtensionIntroducer, GifConstants.ExtensionIntroducer, GifConstants.GraphicControlLabel, 4 // Size
GifConstants.GraphicControlLabel, };
4 // Size
};
writer.Write(intro); writer.Write(intro);
@ -275,8 +278,7 @@ namespace ImageSharp.Formats
/// <param name="image">The <see cref="ImageBase{TColor, TPacked}"/> to be encoded.</param> /// <param name="image">The <see cref="ImageBase{TColor, TPacked}"/> to be encoded.</param>
/// <param name="writer">The stream to write to.</param> /// <param name="writer">The stream to write to.</param>
private void WriteImageDescriptor<TColor, TPacked>(ImageBase<TColor, TPacked> image, EndianBinaryWriter writer) private void WriteImageDescriptor<TColor, TPacked>(ImageBase<TColor, TPacked> image, EndianBinaryWriter writer)
where TColor : struct, IPackedPixel<TPacked> where TColor : struct, IPackedPixel<TPacked> where TPacked : struct
where TPacked : struct
{ {
writer.Write(GifConstants.ImageDescriptorLabel); // 2c writer.Write(GifConstants.ImageDescriptorLabel); // 2c
// TODO: Can we capture this? // TODO: Can we capture this?
@ -306,27 +308,29 @@ namespace ImageSharp.Formats
where TPacked : struct where TPacked : struct
{ {
// Grab the palette and write it to the stream. // Grab the palette and write it to the stream.
TColor[] palette = image.Palette; int pixelCount = image.Palette.Length;
int pixelCount = palette.Length;
// Get max colors for bit depth. // Get max colors for bit depth.
int colorTableLength = (int)Math.Pow(2, this.bitDepth) * 3; int colorTableLength = (int)Math.Pow(2, this.bitDepth) * 3;
byte[] colorTable = new byte[colorTableLength]; byte[] colorTable = ArrayPool<byte>.Shared.Rent(colorTableLength);
Parallel.For( try
0, {
pixelCount, for (int i = 0; i < pixelCount; i++)
i => {
{
int offset = i * 3; int offset = i * 3;
Color color = new Color(palette[i].ToVector4()); image.Palette[i].ToBytes(this.pixelBuffer, 0, ComponentOrder.XYZ);
colorTable[offset] = this.pixelBuffer[0];
colorTable[offset] = color.R; colorTable[offset + 1] = this.pixelBuffer[1];
colorTable[offset + 1] = color.G; colorTable[offset + 2] = this.pixelBuffer[2];
colorTable[offset + 2] = color.B; }
});
writer.Write(colorTable, 0, colorTableLength); writer.Write(colorTable, 0, colorTableLength);
}
finally
{
ArrayPool<byte>.Shared.Return(colorTable);
}
} }
/// <summary> /// <summary>
@ -340,10 +344,10 @@ namespace ImageSharp.Formats
where TColor : struct, IPackedPixel<TPacked> where TColor : struct, IPackedPixel<TPacked>
where TPacked : struct where TPacked : struct
{ {
byte[] indexedPixels = image.Pixels; using (LzwEncoder encoder = new LzwEncoder(image.Pixels, (byte)this.bitDepth))
{
LzwEncoder encoder = new LzwEncoder(indexedPixels, (byte)this.bitDepth); encoder.Encode(writer.BaseStream);
encoder.Encode(writer.BaseStream); }
} }
} }
} }

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

@ -6,6 +6,7 @@
namespace ImageSharp.Formats namespace ImageSharp.Formats
{ {
using System; using System;
using System.Buffers;
using System.IO; using System.IO;
/// <summary> /// <summary>
@ -31,33 +32,70 @@ namespace ImageSharp.Formats
/// Joe Orost (decvax!vax135!petsd!joe) /// Joe Orost (decvax!vax135!petsd!joe)
/// </para> /// </para>
/// </remarks> /// </remarks>
internal sealed class LzwEncoder internal sealed class LzwEncoder : IDisposable
{ {
/// <summary>
/// The end-of-file marker
/// </summary>
private const int Eof = -1; private const int Eof = -1;
/// <summary>
/// The maximum number of bits.
/// </summary>
private const int Bits = 12; private const int Bits = 12;
private const int HashSize = 5003; // 80% occupancy /// <summary>
/// 80% occupancy
/// </summary>
private const int HashSize = 5003;
/// <summary>
/// Mask used when shifting pixel values
/// </summary>
private static readonly int[] Masks =
{
0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF,
0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF
};
/// <summary>
/// The working pixel array
/// </summary>
private readonly byte[] pixelArray; private readonly byte[] pixelArray;
/// <summary>
/// The initial code size.
/// </summary>
private readonly int initialCodeSize; private readonly int initialCodeSize;
private readonly int[] hashTable = new int[HashSize]; /// <summary>
/// The hash table.
private readonly int[] codeTable = new int[HashSize]; /// </summary>
private readonly int[] hashTable;
private readonly int[] masks = /// <summary>
{ /// The code table.
0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF, /// </summary>
0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF private readonly int[] codeTable;
};
/// <summary> /// <summary>
/// Define the storage for the packet accumulator. /// Define the storage for the packet accumulator.
/// </summary> /// </summary>
private readonly byte[] accumulators = new byte[256]; private readonly byte[] accumulators = new byte[256];
/// <summary>
/// A value indicating whether this instance of the given entity has been disposed.
/// </summary>
/// <value><see langword="true"/> if this instance has been disposed; otherwise, <see langword="false"/>.</value>
/// <remarks>
/// If the entity is disposed, it must not be disposed a second
/// time. The isDisposed field is set the first time the entity
/// is disposed. If the isDisposed field is true, then the Dispose()
/// method will not dispose again. This help not to prolong the entity's
/// life in the Garbage Collector.
/// </remarks>
private bool isDisposed = false;
/// <summary> /// <summary>
/// The current pixel /// The current pixel
/// </summary> /// </summary>
@ -99,39 +137,50 @@ namespace ImageSharp.Formats
/// </summary> /// </summary>
private bool clearFlag; private bool clearFlag;
// Algorithm: use open addressing double hashing (no chaining) on the /// <summary>
// prefix code / next character combination. We do a variant of Knuth's /// Algorithm: use open addressing double hashing (no chaining) on the
// algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime /// prefix code / next character combination. We do a variant of Knuth's
// secondary probe. Here, the modular division first probe is gives way /// algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime
// to a faster exclusive-or manipulation. Also do block compression with /// secondary probe. Here, the modular division first probe is gives way
// an adaptive reset, whereby the code table is cleared when the compression /// to a faster exclusive-or manipulation. Also do block compression with
// ratio decreases, but after the table fills. The variable-length output /// an adaptive reset, whereby the code table is cleared when the compression
// codes are re-sized at this point, and a special CLEAR code is generated /// ratio decreases, but after the table fills. The variable-length output
// for the decompressor. Late addition: construct the table according to /// codes are re-sized at this point, and a special CLEAR code is generated
// file size for noticeable speed improvement on small files. Please direct /// for the decompressor. Late addition: construct the table according to
// questions about this implementation to ames!jaw. /// file size for noticeable speed improvement on small files. Please direct
/// questions about this implementation to ames!jaw.
/// </summary>
private int globalInitialBits; private int globalInitialBits;
/// <summary>
/// The clear code.
/// </summary>
private int clearCode; private int clearCode;
/// <summary>
/// The end-of-file code.
/// </summary>
private int eofCode; private int eofCode;
// output /// <summary>
// /// Output the given code.
// Output the given code. /// Inputs:
// Inputs: /// code: A bitCount-bit integer. If == -1, then EOF. This assumes
// code: A bitCount-bit integer. If == -1, then EOF. This assumes /// that bitCount =&lt; wordsize - 1.
// that bitCount =< wordsize - 1. /// Outputs:
// Outputs: /// Outputs code to the file.
// Outputs code to the file. /// Assumptions:
// Assumptions: /// Chars are 8 bits long.
// Chars are 8 bits long. /// Algorithm:
// Algorithm: /// Maintain a BITS character long buffer (so that 8 codes will
// Maintain a BITS character long buffer (so that 8 codes will /// fit in it exactly). Use the VAX insv instruction to insert each
// fit in it exactly). Use the VAX insv instruction to insert each /// code in turn. When the buffer fills up empty it and start over.
// code in turn. When the buffer fills up empty it and start over. /// </summary>
private int currentAccumulator; private int currentAccumulator;
/// <summary>
/// The current bits.
/// </summary>
private int currentBits; private int currentBits;
/// <summary> /// <summary>
@ -148,6 +197,9 @@ namespace ImageSharp.Formats
{ {
this.pixelArray = indexedPixels; this.pixelArray = indexedPixels;
this.initialCodeSize = Math.Max(2, colorDepth); this.initialCodeSize = Math.Max(2, colorDepth);
this.hashTable = ArrayPool<int>.Shared.Rent(HashSize);
this.codeTable = ArrayPool<int>.Shared.Rent(HashSize);
} }
/// <summary> /// <summary>
@ -168,6 +220,13 @@ namespace ImageSharp.Formats
stream.WriteByte(GifConstants.Terminator); stream.WriteByte(GifConstants.Terminator);
} }
/// <inheritdoc />
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
this.Dispose(true);
}
/// <summary> /// <summary>
/// Gets the maximum code value /// Gets the maximum code value
/// </summary> /// </summary>
@ -348,11 +407,6 @@ namespace ImageSharp.Formats
return Eof; return Eof;
} }
if (this.currentPixel == this.pixelArray.Length)
{
return Eof;
}
this.currentPixel++; this.currentPixel++;
return this.pixelArray[this.currentPixel - 1] & 0xff; return this.pixelArray[this.currentPixel - 1] & 0xff;
} }
@ -364,7 +418,7 @@ namespace ImageSharp.Formats
/// <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)
{ {
this.currentAccumulator &= this.masks[this.currentBits]; this.currentAccumulator &= Masks[this.currentBits];
if (this.currentBits > 0) if (this.currentBits > 0)
{ {
@ -415,5 +469,25 @@ namespace ImageSharp.Formats
this.FlushPacket(outs); this.FlushPacket(outs);
} }
} }
/// <summary>
/// Disposes the object and frees resources for the Garbage Collector.
/// </summary>
/// <param name="disposing">If true, the object gets disposed.</param>
private void Dispose(bool disposing)
{
if (this.isDisposed)
{
return;
}
if (disposing)
{
ArrayPool<int>.Shared.Return(this.hashTable);
ArrayPool<int>.Shared.Return(this.codeTable);
}
this.isDisposed = true;
}
} }
} }

11
src/ImageSharp/Quantizers/Octree/OctreeQuantizer.cs

@ -7,7 +7,6 @@ namespace ImageSharp.Quantizers
{ {
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics;
/// <summary> /// <summary>
/// Encapsulates methods to calculate the color palette if an image using an Octree pattern. /// Encapsulates methods to calculate the color palette if an image using an Octree pattern.
@ -183,7 +182,8 @@ namespace ImageSharp.Quantizers
TPacked packed = pixel.PackedValue; TPacked packed = pixel.PackedValue;
// Check if this request is for the same color as the last // Check if this request is for the same color as the last
if (this.previousColor.Equals(packed)) // TODO: We should change our TPacked signature to ensure we can compare values without boxing allocations.
if (this.previousColor.Equals((IEquatable<TPacked>)packed))
{ {
// If so, check if I have a previous node setup. This will only occur if the first color in the image // If so, check if I have a previous node setup. This will only occur if the first color in the image
// happens to be black, with an alpha component of zero. // happens to be black, with an alpha component of zero.
@ -282,11 +282,6 @@ namespace ImageSharp.Quantizers
/// </summary> /// </summary>
protected class OctreeNode protected class OctreeNode
{ {
/// <summary>
/// Vector representing the maximum number of bytes
/// </summary>
private static readonly Vector4 MaxBytes = new Vector4(255);
/// <summary> /// <summary>
/// Pointers to any child nodes /// Pointers to any child nodes
/// </summary> /// </summary>
@ -481,7 +476,7 @@ namespace ImageSharp.Quantizers
/// <summary> /// <summary>
/// Return the palette index for the passed color /// Return the palette index for the passed color
/// </summary> /// </summary>
/// <param name="pixel">The <typeparamref name="TColor"/> representing the pixel.</param> /// <param name="pixel">The pixel data.</param>
/// <param name="level">The level.</param> /// <param name="level">The level.</param>
/// <param name="buffer">The buffer array.</param> /// <param name="buffer">The buffer array.</param>
/// <returns> /// <returns>

3
src/ImageSharp/Quantizers/QuantizedImage.cs

@ -33,8 +33,7 @@ namespace ImageSharp.Quantizers
if (pixels.Length != width * height) if (pixels.Length != width * height)
{ {
throw new ArgumentException( throw new ArgumentException($"Pixel array size must be {nameof(width)} * {nameof(height)}", nameof(pixels));
$"Pixel array size must be {nameof(width)} * {nameof(height)}", nameof(pixels));
} }
this.Width = width; this.Width = width;

Loading…
Cancel
Save