Skip to content

Commit

Permalink
Merge pull request #637 from SixLabors/js/faster-gif
Browse files Browse the repository at this point in the history
Gif and Quantization Improvements
  • Loading branch information
JimBobSquarePants authored Jun 28, 2018
2 parents 6949cde + ad8c2e2 commit d7bd82b
Show file tree
Hide file tree
Showing 25 changed files with 483 additions and 348 deletions.
9 changes: 7 additions & 2 deletions src/ImageSharp/Common/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ namespace SixLabors.ImageSharp
internal static class Constants
{
/// <summary>
/// The epsilon for comparing floating point numbers.
/// The epsilon value for comparing floating point numbers.
/// </summary>
public static readonly float Epsilon = 0.001f;
public static readonly float Epsilon = 0.001F;

/// <summary>
/// The epsilon squared value for comparing floating point numbers.
/// </summary>
public static readonly float EpsilonSquared = Epsilon * Epsilon;
}
}
21 changes: 21 additions & 0 deletions src/ImageSharp/Formats/Gif/GifColorTableMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.

namespace SixLabors.ImageSharp.Formats.Gif
{
/// <summary>
/// Provides enumeration for the available Gif color table modes.
/// </summary>
public enum GifColorTableMode
{
/// <summary>
/// A single color table is calculated from the first frame and reused for subsequent frames.
/// </summary>
Global,

/// <summary>
/// A unique color table is calculated for each frame.
/// </summary>
Local
}
}
5 changes: 5 additions & 0 deletions src/ImageSharp/Formats/Gif/GifEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public sealed class GifEncoder : IImageEncoder, IGifEncoderOptions
/// </summary>
public IQuantizer Quantizer { get; set; } = new OctreeQuantizer();

/// <summary>
/// Gets or sets the color table mode: Global or local.
/// </summary>
public GifColorTableMode ColorTableMode { get; set; }

/// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : struct, IPixel<TPixel>
Expand Down
113 changes: 87 additions & 26 deletions src/ImageSharp/Formats/Gif/GifEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
internal sealed class GifEncoderCore
{
/// <summary>
/// Used for allocating memory during procesing operations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;

/// <summary>
Expand All @@ -27,15 +30,20 @@ internal sealed class GifEncoderCore
private readonly byte[] buffer = new byte[20];

/// <summary>
/// Gets the text encoding used to write comments.
/// The text encoding used to write comments.
/// </summary>
private readonly Encoding textEncoding;

/// <summary>
/// Gets or sets the quantizer used to generate the color palette.
/// The quantizer used to generate the color palette.
/// </summary>
private readonly IQuantizer quantizer;

/// <summary>
/// The color table mode: Global or local.
/// </summary>
private readonly GifColorTableMode colorTableMode;

/// <summary>
/// A flag indicating whether to ingore the metadata when writing the image.
/// </summary>
Expand All @@ -56,6 +64,7 @@ public GifEncoderCore(MemoryAllocator memoryAllocator, IGifEncoderOptions option
this.memoryAllocator = memoryAllocator;
this.textEncoding = options.TextEncoding ?? GifConstants.DefaultEncoding;
this.quantizer = options.Quantizer;
this.colorTableMode = options.ColorTableMode;
this.ignoreMetadata = options.IgnoreMetadata;
}

Expand All @@ -72,45 +81,95 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream)
Guard.NotNull(stream, nameof(stream));

// 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);

// Get the number of bits.
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8);

int index = this.GetTransparentIndex(quantized);

// Write the header.
this.WriteHeader(stream);

// Write the LSD. We'll use local color tables for now.
this.WriteLogicalScreenDescriptor(image, stream, index);
// Write the LSD.
int index = this.GetTransparentIndex(quantized);
bool useGlobalTable = this.colorTableMode.Equals(GifColorTableMode.Global);
this.WriteLogicalScreenDescriptor(image, index, useGlobalTable, stream);

if (useGlobalTable)
{
this.WriteColorTable(quantized, stream);
}

// Write the first frame.
// Write the comments.
this.WriteComments(image.MetaData, stream);

// Write additional frames.
// Write application extension to allow additional frames.
if (image.Frames.Count > 1)
{
this.WriteApplicationExtension(stream, image.MetaData.RepeatCount);
}

if (useGlobalTable)
{
this.EncodeGlobal(image, quantized, index, stream);
}
else
{
this.EncodeLocal(image, quantized, stream);
}

// Clean up.
quantized?.Dispose();
quantized = null;

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

private void EncodeGlobal<TPixel>(Image<TPixel> image, QuantizedFrame<TPixel> quantized, int transparencyIndex, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
var palleteQuantizer = new PaletteQuantizer(this.quantizer.Diffuser);

for (int i = 0; i < image.Frames.Count; i++)
{
ImageFrame<TPixel> frame = image.Frames[i];

this.WriteGraphicalControlExtension(frame.MetaData, transparencyIndex, stream);
this.WriteImageDescriptor(frame, false, stream);

if (i == 0)
{
this.WriteImageData(quantized, stream);
}
else
{
using (QuantizedFrame<TPixel> paletteQuantized = palleteQuantizer.CreateFrameQuantizer(() => quantized.Palette).QuantizeFrame(frame))
{
this.WriteImageData(paletteQuantized, stream);
}
}
}
}

private void EncodeLocal<TPixel>(Image<TPixel> image, QuantizedFrame<TPixel> quantized, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
foreach (ImageFrame<TPixel> frame in image.Frames)
{
if (quantized == null)
{
quantized = this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(frame);
}

this.WriteGraphicalControlExtension(frame.MetaData, stream, this.GetTransparentIndex(quantized));
this.WriteImageDescriptor(frame, stream);
this.WriteGraphicalControlExtension(frame.MetaData, this.GetTransparentIndex(quantized), stream);
this.WriteImageDescriptor(frame, true, stream);
this.WriteColorTable(quantized, stream);
this.WriteImageData(quantized, stream);

quantized?.Dispose();
quantized = null; // So next frame can regenerate it
}

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

/// <summary>
Expand Down Expand Up @@ -159,12 +218,13 @@ private void WriteHeader(Stream stream)
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The image to encode.</param>
/// <param name="stream">The stream to write to.</param>
/// <param name="transparencyIndex">The transparency index to set the default background index to.</param>
private void WriteLogicalScreenDescriptor<TPixel>(Image<TPixel> image, Stream stream, int transparencyIndex)
/// <param name="useGlobalTable">Whether to use a global or local color table.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteLogicalScreenDescriptor<TPixel>(Image<TPixel> image, int transparencyIndex, bool useGlobalTable, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(false, this.bitDepth - 1, false, this.bitDepth - 1);
byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, this.bitDepth - 1, false, this.bitDepth - 1);

var descriptor = new GifLogicalScreenDescriptor(
width: (ushort)image.Width,
Expand Down Expand Up @@ -243,18 +303,18 @@ private void WriteComments(ImageMetaData metadata, Stream stream)
/// Writes the graphics control extension to the stream.
/// </summary>
/// <param name="metaData">The metadata of the image or frame.</param>
/// <param name="stream">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(ImageFrameMetaData metaData, Stream stream, int transparencyIndex)
/// <param name="stream">The stream to write to.</param>
private void WriteGraphicalControlExtension(ImageFrameMetaData metaData, int transparencyIndex, Stream stream)
{
byte packedValue = GifGraphicControlExtension.GetPackedValue(
disposalMethod: metaData.DisposalMethod,
transparencyFlag: transparencyIndex > -1);

var extension = new GifGraphicControlExtension(
packed: packedValue,
transparencyIndex: unchecked((byte)transparencyIndex),
delayTime: (ushort)metaData.FrameDelay);
delayTime: (ushort)metaData.FrameDelay,
transparencyIndex: unchecked((byte)transparencyIndex));

this.WriteExtension(extension, stream);
}
Expand All @@ -281,15 +341,16 @@ public void WriteExtension(IGifExtension extension, Stream stream)
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to be encoded.</param>
/// <param name="hasColorTable">Whether to use the global color table.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteImageDescriptor<TPixel>(ImageFrame<TPixel> image, Stream stream)
private void WriteImageDescriptor<TPixel>(ImageFrame<TPixel> image, bool hasColorTable, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
byte packedValue = GifImageDescriptor.GetPackedValue(
localColorTableFlag: true,
localColorTableFlag: hasColorTable,
interfaceFlag: false,
sortFlag: false,
localColorTableSize: (byte)this.bitDepth); // Note: we subtract 1 from the colorTableSize writing
localColorTableSize: (byte)this.bitDepth);

var descriptor = new GifImageDescriptor(
left: 0,
Expand Down Expand Up @@ -342,9 +403,9 @@ private void WriteColorTable<TPixel>(QuantizedFrame<TPixel> image, Stream stream
private void WriteImageData<TPixel>(QuantizedFrame<TPixel> image, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
using (var encoder = new LzwEncoder(this.memoryAllocator, image.Pixels, (byte)this.bitDepth))
using (var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth))
{
encoder.Encode(stream);
encoder.Encode(image.GetPixelSpan(), stream);
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,10 @@ internal interface IGifEncoderOptions
/// Gets the quantizer used to generate the color palette.
/// </summary>
IQuantizer Quantizer { get; }

/// <summary>
/// Gets the color table mode: Global or local.
/// </summary>
GifColorTableMode ColorTableMode { get; }
}
}
Loading

0 comments on commit d7bd82b

Please sign in to comment.