Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gif and Quantization Improvements #637

Merged
merged 17 commits into from
Jun 28, 2018
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably allow the user to select the baseline frame.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did think about this but if someone creates a custom PaletteQuantizer implementation then it doesn't matter what frame is used. Plus it would vary from image to image.

/// </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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you swear you never coded in Java? 😄

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've read a lot of Java source 😄

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