Browse Source

Working Gif Encoder 🎉

Former-commit-id: cbd69b2e1405f6ba4dd766177ce70f9f8bf08433
Former-commit-id: 18b60afa438ff00a9e4d03b10ee19876d0132563
Former-commit-id: 24c2a39bca3156adf35295a0a9bbeb5c01fd1cff
pull/17/head
James Jackson-South 11 years ago
parent
commit
c4bd0aebb5
  1. 236
      src/ImageProcessor/Formats/Gif/GifEncoder.cs
  2. 8
      src/ImageProcessor/Formats/Gif/Quantizer/OctreeQuantizer.cs
  3. 6
      src/ImageProcessor/Formats/Gif/Quantizer/Quantizer.cs
  4. 8
      src/ImageProcessor/Image.cs
  5. 4
      src/ImageProcessor/ImageBase.cs
  6. 22
      tests/ImageProcessor.Tests/Formats/EncoderDecoderTests.cs
  7. 1
      tests/ImageProcessor.Tests/TestImages/Formats/Gif/ani.gif.REMOVED.git-id
  8. 1
      tests/ImageProcessor.Tests/TestImages/Formats/Gif/ani2.gif.REMOVED.git-id
  9. BIN
      tests/ImageProcessor.Tests/TestImages/Formats/Gif/giphy.gif

236
src/ImageProcessor/Formats/Gif/GifEncoder.cs

@ -7,22 +7,13 @@ namespace ImageProcessor.Formats
{
using System;
using System.IO;
using System.Linq;
/// <summary>
/// The Gif encoder
/// </summary>
public class GifEncoder : IImageEncoder
{
/// <summary>
/// The gif decoder if any used to decode the original image.
/// </summary>
private GifDecoder gifDecoder;
/// <summary>
/// The currently processed image.
/// </summary>
private ImageBase currentImage;
/// <summary>
/// Gets or sets the quality of output for images.
/// </summary>
@ -62,13 +53,6 @@ namespace ImageProcessor.Formats
Image image = (Image)imageBase;
// Try to grab and assign an image decoder.
IImageDecoder decoder = image.CurrentDecoder;
if (decoder.GetType() == typeof(GifDecoder))
{
this.gifDecoder = (GifDecoder)decoder;
}
// Write the header.
// File Header signature and version.
this.WriteString(stream, GifConstants.FileType);
@ -84,63 +68,41 @@ namespace ImageProcessor.Formats
// Write the LSD and check to see if we need a global color table.
// Always true just now.
bool globalColor = this.WriteGlobalLogicalScreenDescriptor(image, stream, bitdepth);
QuantizedImage quantized;
// Should always be true.
if (globalColor)
{
quantized = this.WriteColorTable(imageBase, stream, quality, bitdepth);
}
else
{
// Quantize the image returning a pallete.
IQuantizer quantizer = new OctreeQuantizer(quality.Clamp(1, 255), bitdepth + 1);
quantized = quantizer.Quantize(image);
}
QuantizedImage quantized = this.WriteColorTable(imageBase, stream, quality, bitdepth);
this.WriteGraphicalControlExtension(imageBase, stream);
this.WriteImageDescriptor(quantized, quality, stream);
// TODO: Write Comments
this.WriteApplicationExtension(stream, image.RepeatCount, image.Frames.Count);
this.WriteImageDescriptor(quantized.ToImage(), quality, stream, true);
// TODO: Write Image Info
foreach (ImageFrame frame in image.Frames)
if (image.Frames.Any())
{
this.WriteGraphicalControlExtension(frame, stream);
this.WriteImageDescriptor(imageBase, quality, stream, false);
this.WriteApplicationExtension(stream, image.RepeatCount, image.Frames.Count);
foreach (ImageFrame frame in image.Frames)
{
this.WriteGraphicalControlExtension(frame, stream);
this.WriteFrameImageDescriptor(frame, stream);
}
}
// Cleanup
this.gifDecoder = null;
// TODO: Write Comments extension etc
this.WriteByte(stream, GifConstants.EndIntroducer);
}
/// <summary>
/// Writes the logical screen descriptor to the stream.
/// </summary>
/// <param name="image">The image to encode.</param>
/// <param name="stream">The stream to write to.</param>
/// <param name="bitDepth">The bit depth.</param>
/// <returns>The <see cref="GifLogicalScreenDescriptor"/></returns>
private bool WriteGlobalLogicalScreenDescriptor(Image image, Stream stream, int bitDepth)
{
GifLogicalScreenDescriptor descriptor;
// Try and grab an existing descriptor.
if (this.gifDecoder != null)
GifLogicalScreenDescriptor descriptor = new GifLogicalScreenDescriptor
{
// Ensure the dimensions etc are up to date.
descriptor = this.gifDecoder.CoreDecoder.LogicalScreenDescriptor;
descriptor.Width = (short)image.Width;
descriptor.Height = (short)image.Height;
descriptor.GlobalColorTableFlag = true;
descriptor.GlobalColorTableSize = bitDepth;
}
else
{
descriptor = new GifLogicalScreenDescriptor
{
Width = (short)image.Width,
Height = (short)image.Height,
GlobalColorTableFlag = true,
GlobalColorTableSize = bitDepth
};
}
Width = (short)image.Width,
Height = (short)image.Height,
GlobalColorTableFlag = true,
GlobalColorTableSize = bitDepth
};
this.WriteShort(stream, descriptor.Width);
this.WriteShort(stream, descriptor.Height);
@ -157,6 +119,14 @@ namespace ImageProcessor.Formats
return descriptor.GlobalColorTableFlag;
}
/// <summary>
/// Writes the color table to the stream.
/// </summary>
/// <param name="image">The <see cref="ImageBase"/> to encode.</param>
/// <param name="stream">The stream to write to.</param>
/// <param name="quality">The quality (number of colors) to encode the image to.</param>
/// <param name="bitDepth">The bit depth.</param>
/// <returns>The <see cref="QuantizedImage"/></returns>
private QuantizedImage WriteColorTable(ImageBase image, Stream stream, int quality, int bitDepth)
{
// Quantize the image returning a pallete.
@ -166,7 +136,9 @@ namespace ImageProcessor.Formats
// Grab the pallete and write it to the stream.
Bgra[] pallete = quantizedImage.Palette;
int pixelCount = pallete.Length;
int colorTableLength = pixelCount * 3;
// Get max colors for bit depth.
int colorTableLength = (int)Math.Pow(2, bitDepth) * 3;
byte[] colorTable = new byte[colorTableLength];
for (int i = 0; i < pixelCount; i++)
@ -183,40 +155,30 @@ namespace ImageProcessor.Formats
return quantizedImage;
}
/// <summary>
/// Writes the graphics control extension to the stream.
/// </summary>
/// <param name="image">The <see cref="ImageBase"/> to encode.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteGraphicalControlExtension(ImageBase image, Stream stream)
{
GifGraphicsControlExtension extension;
// Calculate the quality.
int quality = this.Quality > 0 ? this.Quality : image.Quality;
quality = quality > 0 ? quality.Clamp(1, 256) : 256;
// Try and grab an existing descriptor.
// TODO: Check whether we need to.
if (this.gifDecoder != null)
{
// Ensure the dimensions etc are up to date.
extension = this.gifDecoder.CoreDecoder.GraphicsControlExtension;
extension.TransparencyFlag = quality > 1;
extension.TransparencyIndex = quality - 1; // Quantizer set last as transparent.
extension.DelayTime = image.FrameDelay;
}
else
{
// TODO: Check transparency logic.
bool hasTransparent = quality > 1;
DisposalMethod disposalMethod = hasTransparent
? DisposalMethod.RestoreToBackground
: DisposalMethod.Unspecified;
// TODO: Check transparency logic.
bool hasTransparent = quality > 1;
DisposalMethod disposalMethod = hasTransparent
? DisposalMethod.RestoreToBackground
: DisposalMethod.Unspecified;
extension = new GifGraphicsControlExtension()
{
DisposalMethod = disposalMethod,
TransparencyFlag = hasTransparent,
TransparencyIndex = quality - 1,
DelayTime = image.FrameDelay
};
}
GifGraphicsControlExtension extension = new GifGraphicsControlExtension()
{
DisposalMethod = disposalMethod,
TransparencyFlag = hasTransparent,
TransparencyIndex = quality - 1, // Quantizer sets last index as transparent.
DelayTime = image.FrameDelay
};
this.WriteByte(stream, GifConstants.ExtensionIntroducer);
this.WriteByte(stream, GifConstants.GraphicControlLabel);
@ -233,6 +195,12 @@ namespace ImageProcessor.Formats
this.WriteByte(stream, GifConstants.Terminator);
}
/// <summary>
/// Writes the application exstension to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="repeatCount">The animated image repeat count.</param>
/// <param name="frames">Th number of image frames.</param>
private void WriteApplicationExtension(Stream stream, ushort repeatCount, int frames)
{
// Application Extension Header
@ -254,7 +222,13 @@ namespace ImageProcessor.Formats
}
}
private void WriteImageDescriptor(ImageBase image, int quality, Stream stream, bool first)
/// <summary>
/// Writes the image descriptor to the stream.
/// </summary>
/// <param name="image">The <see cref="QuantizedImage"/> containing indexed pixels.</param>
/// <param name="quality">The quality (number of colors) to encode the image to.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteImageDescriptor(QuantizedImage image, int quality, Stream stream)
{
this.WriteByte(stream, GifConstants.ImageDescriptorLabel); // 2c
// TODO: Can we capture this?
@ -263,43 +237,59 @@ namespace ImageProcessor.Formats
this.WriteShort(stream, image.Width);
this.WriteShort(stream, image.Height);
if (first)
{
// Calculate the quality.
int bitdepth = this.GetBitsNeededForColorDepth(quality);
// Calculate the quality.
int bitdepth = this.GetBitsNeededForColorDepth(quality);
// No LCT use GCT.
this.WriteByte(stream, 0);
// No LCT use GCT.
this.WriteByte(stream, 0);
// Write the image data. Pixels have already been quantized.
this.WriteImageData(image, stream, bitdepth);
}
else
{
// Calculate the quality.
quality = this.Quality > 0 ? this.Quality : image.Quality;
quality = quality > 0 ? quality.Clamp(1, 256) : 256;
int bitdepth = this.GetBitsNeededForColorDepth(quality);
int packed = 0x80 | // 1: Local color table flag = 1 (LCT used)
0x00 | // 2: Interlace flag 0
0x00 | // 3: Sort flag 0
0 | // 4-5: Reserved
bitdepth - 1;
this.WriteByte(stream, packed);
// Now immediately follow with the color table.
QuantizedImage quantized = this.WriteColorTable(image, stream, quality, bitdepth);
this.WriteImageData(quantized.ToImage(), stream, bitdepth);
}
// Write the image data..
this.WriteImageData(image, stream, bitdepth);
}
this.WriteByte(stream, GifConstants.EndIntroducer);
/// <summary>
/// Writes the image descriptor to the stream.
/// </summary>
/// <param name="image">The <see cref="ImageBase"/> to be encoded.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteFrameImageDescriptor(ImageBase image, Stream stream)
{
this.WriteByte(stream, GifConstants.ImageDescriptorLabel); // 2c
// TODO: Can we capture this?
this.WriteShort(stream, 0); // Left position
this.WriteShort(stream, 0); // Top position
this.WriteShort(stream, image.Width);
this.WriteShort(stream, image.Height);
// Calculate the quality.
int quality = this.Quality > 0 ? this.Quality : image.Quality;
quality = quality > 0 ? quality.Clamp(1, 256) : 256;
int bitdepth = this.GetBitsNeededForColorDepth(quality);
int packed = 0x80 | // 1: Local color table flag = 1 (LCT used)
0x00 | // 2: Interlace flag 0
0x00 | // 3: Sort flag 0
0 | // 4-5: Reserved
bitdepth - 1;
this.WriteByte(stream, packed);
// Now immediately follow with the color table.
QuantizedImage quantized = this.WriteColorTable(image, stream, quality, bitdepth);
this.WriteImageData(quantized, stream, bitdepth);
}
private void WriteImageData(ImageBase image, Stream stream, int bitDepth)
/// <summary>
/// Writes the image pixel data to the stream.
/// </summary>
/// <param name="image">The <see cref="QuantizedImage"/> containing indexed pixels.</param>
/// <param name="stream">The stream to write to.</param>
/// <param name="bitDepth">The bit depth of the image.</param>
private void WriteImageData(QuantizedImage image, Stream stream, int bitDepth)
{
LzwEncoder encoder = new LzwEncoder(image.Pixels, (byte)bitDepth);
byte[] indexedPixels = image.Pixels;
LzwEncoder encoder = new LzwEncoder(indexedPixels, (byte)bitDepth);
encoder.Encode(stream);
this.WriteByte(stream, GifConstants.Terminator);

8
src/ImageProcessor/Formats/Gif/Quantizer/OctreeQuantizer.cs

@ -338,7 +338,7 @@ namespace ImageProcessor.Formats
private int paletteIndex;
/// <summary>
/// Initializes a new instance of the <see cref="OctreeNode"/> class.
/// Initializes a new instance of the <see cref="OctreeNode"/> class.
/// </summary>
/// <param name="level">
/// The level in the tree = 0 - 7
@ -415,7 +415,7 @@ namespace ImageProcessor.Formats
if (null == child)
{
// Create a new child node & store in the array
// Create a new child node and store it in the array
child = new OctreeNode(level + 1, colorBits, octree);
this.children[index] = child;
}
@ -511,8 +511,8 @@ namespace ImageProcessor.Formats
{
int shift = 7 - level;
int pixelIndex = ((pixel.R & Mask[level]) >> (shift - 2)) |
((pixel.G & Mask[level]) >> (shift - 1)) |
((pixel.B & Mask[level]) >> shift);
((pixel.G & Mask[level]) >> (shift - 1)) |
((pixel.B & Mask[level]) >> shift);
if (null != this.children[pixelIndex])
{

6
src/ImageProcessor/Formats/Gif/Quantizer/Quantizer.cs

@ -61,9 +61,11 @@ namespace ImageProcessor.Formats
byte[] quantizedPixels = new byte[width * height];
List<Bgra> palette = this.GetPalette();
this.SecondPass(imageBase, quantizedPixels, width, height);
return new QuantizedImage(width, height, this.GetPalette().ToArray(), quantizedPixels);
return new QuantizedImage(width, height, palette.ToArray(), quantizedPixels);
}
/// <summary>
@ -158,4 +160,4 @@ namespace ImageProcessor.Formats
/// </returns>
protected abstract List<Bgra> GetPalette();
}
}
}

8
src/ImageProcessor/Image.cs

@ -232,11 +232,6 @@ namespace ImageProcessor
/// <value>A list of image properties.</value>
public IList<ImageProperty> Properties { get; } = new List<ImageProperty>();
/// <summary>
/// The current decoder
/// </summary>
internal IImageDecoder CurrentDecoder { get; set; }
/// <summary>
/// Loads the image from the given stream.
/// </summary>
@ -276,8 +271,7 @@ namespace ImageProcessor
IImageDecoder decoder = decoders.FirstOrDefault(x => x.IsSupportedFileFormat(header));
if (decoder != null)
{
this.CurrentDecoder = decoder;
this.CurrentDecoder.Decode(this, stream);
decoder.Decode(this, stream);
return;
}
}

4
src/ImageProcessor/ImageBase.cs

@ -145,7 +145,7 @@ namespace ImageProcessor
throw new ArgumentOutOfRangeException(nameof(x), "Value cannot be less than zero or greater than the bitmap width.");
}
if ((y < 0) || (y >= this.Width))
if ((y < 0) || (y >= this.Height))
{
throw new ArgumentOutOfRangeException(nameof(y), "Value cannot be less than zero or greater than the bitmap height.");
}
@ -163,7 +163,7 @@ namespace ImageProcessor
throw new ArgumentOutOfRangeException(nameof(x), "Value cannot be less than zero or greater than the bitmap width.");
}
if ((y < 0) || (y >= this.Width))
if ((y < 0) || (y >= this.Height))
{
throw new ArgumentOutOfRangeException(nameof(y), "Value cannot be less than zero or greater than the bitmap height.");
}

22
tests/ImageProcessor.Tests/Formats/EncoderDecoderTests.cs

@ -11,14 +11,14 @@
public class EncoderDecoderTests
{
[Theory]
//[InlineData("TestImages/Car.bmp")]
//[InlineData("TestImages/Portrait.png")]
//[InlineData("../../TestImages/Formats/Jpg/Backdrop.jpg")]
[InlineData("../../TestImages/Formats/Jpg/Backdrop.jpg")]
[InlineData("../../TestImages/Formats/Gif/a.gif")]
//[InlineData("../../TestImages/Formats/Gif/leaf.gif")]
//[InlineData("TestImages/Windmill.gif")]
//[InlineData("../../TestImages/Formats/Bmp/Car.bmp")]
//[InlineData("../../TestImages/Formats/Png/cmyk.png")]
[InlineData("../../TestImages/Formats/Gif/leaf.gif")]
[InlineData("../../TestImages/Formats/Gif/ani.gif")]
[InlineData("../../TestImages/Formats/Gif/ani2.gif")]
[InlineData("../../TestImages/Formats/Gif/giphy.gif")]
[InlineData("../../TestImages/Formats/Bmp/Car.bmp")]
[InlineData("../../TestImages/Formats/Png/cmyk.png")]
public void DecodeThenEncodeImageFromStreamShouldSucceed(string filename)
{
if (!Directory.Exists("Encoded"))
@ -29,7 +29,7 @@
FileStream stream = File.OpenRead(filename);
Stopwatch watch = Stopwatch.StartNew();
Image image = new Image(stream);
string encodedFilename = "Encoded/" + Path.GetFileName(filename);
//if (!image.IsAnimated)
@ -62,8 +62,8 @@
[Theory]
[InlineData("../../TestImages/Formats/Jpg/Backdrop.jpg")]
[InlineData("../../TestImages/Formats/Bmp/Car.bmp")]
[InlineData("../../TestImages/Formats/Png/cmyk.png")]
//[InlineData("../../TestImages/Formats/Bmp/Car.bmp")]
//[InlineData("../../TestImages/Formats/Png/cmyk.png")]
public void QuantizedImageShouldPreserveMaximumColorPrecision(string filename)
{
if (!Directory.Exists("Quantized"))
@ -74,7 +74,7 @@
Image image = new Image(File.OpenRead(filename));
IQuantizer quantizer = new OctreeQuantizer();
QuantizedImage quantizedImage = quantizer.Quantize(image);
var pixel = quantizedImage.Pixels;
using (FileStream output = File.OpenWrite($"Quantized/{ Path.GetFileName(filename) }"))
{
IImageEncoder encoder = Image.Encoders.First(e => e.IsSupportedFileExtension(Path.GetExtension(filename)));

1
tests/ImageProcessor.Tests/TestImages/Formats/Gif/ani.gif.REMOVED.git-id

@ -0,0 +1 @@
a0cc93222effb5feec0d1a1dc45efd0c5af77450

1
tests/ImageProcessor.Tests/TestImages/Formats/Gif/ani2.gif.REMOVED.git-id

@ -0,0 +1 @@
c2c7a5fcc0f00cdef39dacd7df0816137b3d63a3

BIN
tests/ImageProcessor.Tests/TestImages/Formats/Gif/giphy.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Loading…
Cancel
Save