mirror of https://github.com/SixLabors/ImageSharp
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
454 lines
16 KiB
454 lines
16 KiB
// --------------------------------------------------------------------------------------------------------------------
|
|
// <copyright file="GifEncoder.cs" company="James South">
|
|
// Copyright (c) James South.
|
|
// Licensed under the Apache License, Version 2.0.
|
|
// </copyright>
|
|
// <summary>
|
|
// Encodes multiple images as an animated gif to a stream.
|
|
// <remarks>
|
|
// Always wire this up in a using block.
|
|
// Disposing the encoder will complete the file.
|
|
// Uses default .NET GIF encoding and adds animation headers.
|
|
// Adapted from <see href="http://github.com/DataDink/Bumpkit/blob/master/BumpKit/BumpKit/GifEncoder.cs"/>
|
|
// </remarks>
|
|
// </summary>
|
|
// --------------------------------------------------------------------------------------------------------------------
|
|
|
|
namespace ImageProcessor.Imaging.Formats
|
|
{
|
|
using System;
|
|
using System.Drawing.Imaging;
|
|
using System.IO;
|
|
using System.Linq;
|
|
|
|
/// <summary>
|
|
/// Encodes multiple images as an animated gif to a stream.
|
|
/// <remarks>
|
|
/// Always wire this up in a using block.
|
|
/// Disposing the encoder will complete the file.
|
|
/// Uses default .NET GIF encoding and adds animation headers.
|
|
/// Adapted from <see href="http://github.com/DataDink/Bumpkit/blob/master/BumpKit/BumpKit/GifEncoder.cs"/>
|
|
/// </remarks>
|
|
/// </summary>
|
|
public class GifEncoder : IDisposable
|
|
{
|
|
#region Constants
|
|
/// <summary>
|
|
/// The application block size.
|
|
/// </summary>
|
|
private const byte ApplicationBlockSize = 0x0b;
|
|
|
|
/// <summary>
|
|
/// The application extension block identifier.
|
|
/// </summary>
|
|
private const int ApplicationExtensionBlockIdentifier = 0xff21;
|
|
|
|
/// <summary>
|
|
/// The application identification.
|
|
/// </summary>
|
|
private const string ApplicationIdentification = "NETSCAPE2.0";
|
|
|
|
/// <summary>
|
|
/// The file trailer.
|
|
/// </summary>
|
|
private const byte FileTrailer = 0x3b;
|
|
|
|
/// <summary>
|
|
/// The file type.
|
|
/// </summary>
|
|
private const string FileType = "GIF";
|
|
|
|
/// <summary>
|
|
/// The file version.
|
|
/// </summary>
|
|
private const string FileVersion = "89a";
|
|
|
|
/// <summary>
|
|
/// The graphic control extension block identifier.
|
|
/// </summary>
|
|
private const int GraphicControlExtensionBlockIdentifier = 0xf921;
|
|
|
|
/// <summary>
|
|
/// The graphic control extension block size.
|
|
/// </summary>
|
|
private const byte GraphicControlExtensionBlockSize = 0x04;
|
|
|
|
/// <summary>
|
|
/// The source color block length.
|
|
/// </summary>
|
|
private const long SourceColorBlockLength = 768;
|
|
|
|
/// <summary>
|
|
/// The source color block position.
|
|
/// </summary>
|
|
private const long SourceColorBlockPosition = 13;
|
|
|
|
/// <summary>
|
|
/// The source global color info position.
|
|
/// </summary>
|
|
private const long SourceGlobalColorInfoPosition = 10;
|
|
|
|
/// <summary>
|
|
/// The source graphic control extension length.
|
|
/// </summary>
|
|
private const long SourceGraphicControlExtensionLength = 8;
|
|
|
|
/// <summary>
|
|
/// The source graphic control extension position.
|
|
/// </summary>
|
|
private const long SourceGraphicControlExtensionPosition = 781;
|
|
|
|
/// <summary>
|
|
/// The source image block header length.
|
|
/// </summary>
|
|
private const long SourceImageBlockHeaderLength = 11;
|
|
|
|
/// <summary>
|
|
/// The source image block position.
|
|
/// </summary>
|
|
private const long SourceImageBlockPosition = 789;
|
|
#endregion
|
|
|
|
#region Fields
|
|
/// <summary>
|
|
/// The stream.
|
|
/// </summary>
|
|
// ReSharper disable once FieldCanBeMadeReadOnly.Local
|
|
private MemoryStream inputStream;
|
|
|
|
/// <summary>
|
|
/// The height.
|
|
/// </summary>
|
|
private int? height;
|
|
|
|
/// <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;
|
|
|
|
/// <summary>
|
|
/// The is first image.
|
|
/// </summary>
|
|
private bool isFirstImage = true;
|
|
|
|
/// <summary>
|
|
/// The repeat count.
|
|
/// </summary>
|
|
private int? repeatCount;
|
|
|
|
/// <summary>
|
|
/// The width.
|
|
/// </summary>
|
|
private int? width;
|
|
#endregion
|
|
|
|
#region Constructors
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="GifEncoder"/> class.
|
|
/// </summary>
|
|
/// <param name="stream">
|
|
/// The stream that will be written to.
|
|
/// </param>
|
|
/// <param name="width">
|
|
/// Sets the width for this gif or null to use the first frame's width.
|
|
/// </param>
|
|
/// <param name="height">
|
|
/// Sets the height for this gif or null to use the first frame's height.
|
|
/// </param>
|
|
/// <param name="repeatCount">
|
|
/// The number of times to repeat the animation.
|
|
/// </param>
|
|
public GifEncoder(MemoryStream stream, int? width = null, int? height = null, int? repeatCount = null)
|
|
{
|
|
this.inputStream = stream;
|
|
this.width = width;
|
|
this.height = height;
|
|
this.repeatCount = repeatCount;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finalizes an instance of the <see cref="GifEncoder"/> class.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Use C# destructor syntax for finalization code.
|
|
/// This destructor will run only if the Dispose method
|
|
/// does not get called.
|
|
/// It gives your base class the opportunity to finalize.
|
|
/// Do not provide destructors in types derived from this class.
|
|
/// </remarks>
|
|
~GifEncoder()
|
|
{
|
|
// Do not re-create Dispose clean-up code here.
|
|
// Calling Dispose(false) is optimal in terms of
|
|
// readability and maintainability.
|
|
this.Dispose(false);
|
|
}
|
|
#endregion
|
|
|
|
#region Public Methods and Operators
|
|
/// <summary>
|
|
/// Adds a frame to the gif.
|
|
/// </summary>
|
|
/// <param name="frame">
|
|
/// The <see cref="GifFrame"/> containing the image.
|
|
/// </param>
|
|
public void AddFrame(GifFrame frame)
|
|
{
|
|
using (MemoryStream gifStream = new MemoryStream())
|
|
{
|
|
frame.Image.Save(gifStream, ImageFormat.Gif);
|
|
if (this.isFirstImage)
|
|
{
|
|
// Steal the global color table info
|
|
this.WriteHeaderBlock(gifStream, frame.Image.Width, frame.Image.Height);
|
|
}
|
|
|
|
this.WriteGraphicControlBlock(gifStream, frame.Delay);
|
|
this.WriteImageBlock(gifStream, !this.isFirstImage, frame.X, frame.Y, frame.Image.Width, frame.Image.Height);
|
|
}
|
|
|
|
this.isFirstImage = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disposes the object and frees resources for the Garbage Collector.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
this.Dispose(true);
|
|
|
|
// This object will be cleaned up by the Dispose method.
|
|
// Therefore, you should call GC.SupressFinalize to
|
|
// take this object off the finalization queue
|
|
// and prevent finalization code for this object
|
|
// from executing a second time.
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
#endregion
|
|
|
|
#region Methods
|
|
/// <summary>
|
|
/// Disposes the object and frees resources for the Garbage Collector.
|
|
/// </summary>
|
|
/// <param name="disposing">
|
|
/// If true, the object gets disposed.
|
|
/// </param>
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (this.isDisposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (disposing)
|
|
{
|
|
// Complete Application Block
|
|
this.WriteByte(0);
|
|
|
|
// Complete File
|
|
this.WriteByte(FileTrailer);
|
|
|
|
// Push the data
|
|
this.inputStream.Flush();
|
|
}
|
|
|
|
// Call the appropriate methods to clean up
|
|
// unmanaged resources here.
|
|
// Note disposing is done.
|
|
this.isDisposed = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes the header block of the animated gif to the stream.
|
|
/// </summary>
|
|
/// <param name="sourceGif">
|
|
/// The source gif.
|
|
/// </param>
|
|
/// <param name="w">
|
|
/// The width of the image.
|
|
/// </param>
|
|
/// <param name="h">
|
|
/// The height of the image.
|
|
/// </param>
|
|
private void WriteHeaderBlock(Stream sourceGif, int w, int h)
|
|
{
|
|
int count = this.repeatCount.GetValueOrDefault(0);
|
|
|
|
// File Header signature and version.
|
|
this.WriteString(FileType);
|
|
this.WriteString(FileVersion);
|
|
|
|
// Write the logical screen descriptor.
|
|
this.WriteShort(this.width.GetValueOrDefault(w)); // Initial Logical Width
|
|
this.WriteShort(this.height.GetValueOrDefault(h)); // Initial Logical Height
|
|
|
|
// Read the global color table info.
|
|
sourceGif.Position = SourceGlobalColorInfoPosition;
|
|
this.WriteByte(sourceGif.ReadByte());
|
|
|
|
this.WriteByte(0); // Background Color Index
|
|
this.WriteByte(0); // Pixel aspect ratio
|
|
this.WriteColorTable(sourceGif);
|
|
|
|
// The different browsers interpret the spec differently when adding a loop.
|
|
// If the loop count is one IE and FF < 3 (incorrectly) loop an extra number of times.
|
|
// Removing the Netscape header should fix this.
|
|
if (count > -1 && count != 1)
|
|
{
|
|
// Application Extension Header
|
|
this.WriteShort(ApplicationExtensionBlockIdentifier);
|
|
this.WriteByte(ApplicationBlockSize);
|
|
this.WriteString(ApplicationIdentification);
|
|
this.WriteByte(3); // Application block length
|
|
this.WriteByte(1);
|
|
this.WriteShort(count); // Repeat count for images.
|
|
this.WriteByte(0); // Terminator
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The write byte.
|
|
/// </summary>
|
|
/// <param name="value">
|
|
/// The value.
|
|
/// </param>
|
|
private void WriteByte(int value)
|
|
{
|
|
this.inputStream.WriteByte(Convert.ToByte(value));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The write color table.
|
|
/// </summary>
|
|
/// <param name="sourceGif">
|
|
/// The source gif.
|
|
/// </param>
|
|
private void WriteColorTable(Stream sourceGif)
|
|
{
|
|
sourceGif.Position = SourceColorBlockPosition; // Locating the image color table
|
|
byte[] colorTable = new byte[SourceColorBlockLength];
|
|
sourceGif.Read(colorTable, 0, colorTable.Length);
|
|
this.inputStream.Write(colorTable, 0, colorTable.Length);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The write graphic control block.
|
|
/// </summary>
|
|
/// <param name="sourceGif">
|
|
/// The source gif.
|
|
/// </param>
|
|
/// <param name="frameDelay">
|
|
/// The frame delay.
|
|
/// </param>
|
|
private void WriteGraphicControlBlock(Stream sourceGif, int frameDelay)
|
|
{
|
|
sourceGif.Position = SourceGraphicControlExtensionPosition; // Locating the source GCE
|
|
byte[] blockhead = new byte[SourceGraphicControlExtensionLength];
|
|
sourceGif.Read(blockhead, 0, blockhead.Length); // Reading source GCE
|
|
|
|
this.WriteShort(GraphicControlExtensionBlockIdentifier); // Identifier
|
|
this.WriteByte(GraphicControlExtensionBlockSize); // Block Size
|
|
this.WriteByte(blockhead[3] & 0xf7 | 0x08); // Setting disposal flag
|
|
this.WriteShort(Convert.ToInt32(frameDelay / 10.0f)); // Setting frame delay
|
|
this.WriteByte(blockhead[6]); // Transparent color index
|
|
this.WriteByte(0); // Terminator
|
|
}
|
|
|
|
/// <summary>
|
|
/// The write image block.
|
|
/// </summary>
|
|
/// <param name="sourceGif">
|
|
/// The source gif.
|
|
/// </param>
|
|
/// <param name="includeColorTable">
|
|
/// The include color table.
|
|
/// </param>
|
|
/// <param name="x">
|
|
/// The x position to write the image block.
|
|
/// </param>
|
|
/// <param name="y">
|
|
/// The y position to write the image block.
|
|
/// </param>
|
|
/// <param name="h">
|
|
/// The height of the image block.
|
|
/// </param>
|
|
/// <param name="w">
|
|
/// The width of the image block.
|
|
/// </param>
|
|
private void WriteImageBlock(Stream sourceGif, bool includeColorTable, int x, int y, int h, int w)
|
|
{
|
|
// Local Image Descriptor
|
|
sourceGif.Position = SourceImageBlockPosition; // Locating the image block
|
|
byte[] header = new byte[SourceImageBlockHeaderLength];
|
|
sourceGif.Read(header, 0, header.Length);
|
|
this.WriteByte(header[0]); // Separator
|
|
this.WriteShort(x); // Position X
|
|
this.WriteShort(y); // Position Y
|
|
this.WriteShort(h); // Height
|
|
this.WriteShort(w); // Width
|
|
|
|
if (includeColorTable)
|
|
{
|
|
// If first frame, use global color table - else use local
|
|
sourceGif.Position = SourceGlobalColorInfoPosition;
|
|
this.WriteByte(sourceGif.ReadByte() & 0x3f | 0x80); // Enabling local color table
|
|
this.WriteColorTable(sourceGif);
|
|
}
|
|
else
|
|
{
|
|
this.WriteByte(header[9] & 0x07 | 0x07); // Disabling local color table
|
|
}
|
|
|
|
this.WriteByte(header[10]); // LZW Min Code Size
|
|
|
|
// Read/Write image data
|
|
sourceGif.Position = SourceImageBlockPosition + SourceImageBlockHeaderLength;
|
|
|
|
int dataLength = sourceGif.ReadByte();
|
|
while (dataLength > 0)
|
|
{
|
|
byte[] imgData = new byte[dataLength];
|
|
sourceGif.Read(imgData, 0, dataLength);
|
|
|
|
this.inputStream.WriteByte(Convert.ToByte(dataLength));
|
|
this.inputStream.Write(imgData, 0, dataLength);
|
|
dataLength = sourceGif.ReadByte();
|
|
}
|
|
|
|
this.inputStream.WriteByte(0); // Terminator
|
|
}
|
|
|
|
/// <summary>
|
|
/// The write short.
|
|
/// </summary>
|
|
/// <param name="value">
|
|
/// The value.
|
|
/// </param>
|
|
private void WriteShort(int value)
|
|
{
|
|
// Leave only one significant byte.
|
|
this.inputStream.WriteByte(Convert.ToByte(value & 0xff));
|
|
this.inputStream.WriteByte(Convert.ToByte((value >> 8) & 0xff));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The write string.
|
|
/// </summary>
|
|
/// <param name="value">
|
|
/// The value.
|
|
/// </param>
|
|
private void WriteString(string value)
|
|
{
|
|
this.inputStream.Write(value.ToArray().Select(c => (byte)c).ToArray(), 0, value.Length);
|
|
}
|
|
#endregion
|
|
}
|
|
}
|