Browse Source

Optimize LZW Encoder

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

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

@ -6,6 +6,7 @@
namespace ImageSharp.Formats
{
using System;
using System.Buffers;
using System.IO;
/// <summary>
@ -31,33 +32,70 @@ namespace ImageSharp.Formats
/// Joe Orost (decvax!vax135!petsd!joe)
/// </para>
/// </remarks>
internal sealed class LzwEncoder
internal sealed class LzwEncoder : IDisposable
{
/// <summary>
/// The end-of-file marker
/// </summary>
private const int Eof = -1;
/// <summary>
/// The maximum number of bits.
/// </summary>
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;
/// <summary>
/// The initial code size.
/// </summary>
private readonly int initialCodeSize;
private readonly int[] hashTable = new int[HashSize];
private readonly int[] codeTable = new int[HashSize];
/// <summary>
/// The hash table.
/// </summary>
private readonly int[] hashTable;
private readonly int[] masks =
{
0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF,
0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF
};
/// <summary>
/// The code table.
/// </summary>
private readonly int[] codeTable;
/// <summary>
/// Define the storage for the packet accumulator.
/// </summary>
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>
/// The current pixel
/// </summary>
@ -99,39 +137,50 @@ namespace ImageSharp.Formats
/// </summary>
private bool clearFlag;
// Algorithm: use open addressing double hashing (no chaining) on the
// prefix code / next character combination. We do a variant of Knuth's
// algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime
// secondary probe. Here, the modular division first probe is gives way
// to a faster exclusive-or manipulation. Also do block compression with
// an adaptive reset, whereby the code table is cleared when the compression
// ratio decreases, but after the table fills. The variable-length output
// codes are re-sized at this point, and a special CLEAR code is generated
// for the decompressor. Late addition: construct the table according to
// file size for noticeable speed improvement on small files. Please direct
// questions about this implementation to ames!jaw.
/// <summary>
/// Algorithm: use open addressing double hashing (no chaining) on the
/// prefix code / next character combination. We do a variant of Knuth's
/// algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime
/// secondary probe. Here, the modular division first probe is gives way
/// to a faster exclusive-or manipulation. Also do block compression with
/// an adaptive reset, whereby the code table is cleared when the compression
/// ratio decreases, but after the table fills. The variable-length output
/// codes are re-sized at this point, and a special CLEAR code is generated
/// for the decompressor. Late addition: construct the table according to
/// file size for noticeable speed improvement on small files. Please direct
/// questions about this implementation to ames!jaw.
/// </summary>
private int globalInitialBits;
/// <summary>
/// The clear code.
/// </summary>
private int clearCode;
/// <summary>
/// The end-of-file code.
/// </summary>
private int eofCode;
// output
//
// Output the given code.
// Inputs:
// code: A bitCount-bit integer. If == -1, then EOF. This assumes
// that bitCount =< wordsize - 1.
// Outputs:
// Outputs code to the file.
// Assumptions:
// Chars are 8 bits long.
// Algorithm:
// Maintain a BITS character long buffer (so that 8 codes will
// 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.
/// <summary>
/// Output the given code.
/// Inputs:
/// code: A bitCount-bit integer. If == -1, then EOF. This assumes
/// that bitCount =&lt; wordsize - 1.
/// Outputs:
/// Outputs code to the file.
/// Assumptions:
/// Chars are 8 bits long.
/// Algorithm:
/// Maintain a BITS character long buffer (so that 8 codes will
/// 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.
/// </summary>
private int currentAccumulator;
/// <summary>
/// The current bits.
/// </summary>
private int currentBits;
/// <summary>
@ -148,6 +197,9 @@ namespace ImageSharp.Formats
{
this.pixelArray = indexedPixels;
this.initialCodeSize = Math.Max(2, colorDepth);
this.hashTable = ArrayPool<int>.Shared.Rent(HashSize);
this.codeTable = ArrayPool<int>.Shared.Rent(HashSize);
}
/// <summary>
@ -168,6 +220,13 @@ namespace ImageSharp.Formats
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>
/// Gets the maximum code value
/// </summary>
@ -348,11 +407,6 @@ namespace ImageSharp.Formats
return Eof;
}
if (this.currentPixel == this.pixelArray.Length)
{
return Eof;
}
this.currentPixel++;
return this.pixelArray[this.currentPixel - 1] & 0xff;
}
@ -364,7 +418,7 @@ namespace ImageSharp.Formats
/// <param name="outs">The stream to write to.</param>
private void Output(int code, Stream outs)
{
this.currentAccumulator &= this.masks[this.currentBits];
this.currentAccumulator &= Masks[this.currentBits];
if (this.currentBits > 0)
{
@ -415,5 +469,25 @@ namespace ImageSharp.Formats
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.Collections.Generic;
using System.Numerics;
/// <summary>
/// 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;
// 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
// happens to be black, with an alpha component of zero.
@ -282,11 +282,6 @@ namespace ImageSharp.Quantizers
/// </summary>
protected class OctreeNode
{
/// <summary>
/// Vector representing the maximum number of bytes
/// </summary>
private static readonly Vector4 MaxBytes = new Vector4(255);
/// <summary>
/// Pointers to any child nodes
/// </summary>
@ -481,7 +476,7 @@ namespace ImageSharp.Quantizers
/// <summary>
/// Return the palette index for the passed color
/// </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="buffer">The buffer array.</param>
/// <returns>

3
src/ImageSharp/Quantizers/QuantizedImage.cs

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

Loading…
Cancel
Save