PageRenderTime 51ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/src/ImageProcessor/Imaging/GifEncoder.cs

https://github.com/Abhimanyu-Jana/ImageProcessor
C# | 456 lines | 178 code | 51 blank | 227 comment | 7 complexity | 80ca616847ed4a448484d3a073718b70 MD5 | raw file
  1. // --------------------------------------------------------------------------------------------------------------------
  2. // <copyright file="GifEncoder.cs" company="James South">
  3. // Copyright (c) James South.
  4. // Licensed under the Apache License, Version 2.0.
  5. // </copyright>
  6. // <summary>
  7. // Encodes multiple images as an animated gif to a stream.
  8. // <remarks>
  9. // Always wire this up in a using block.
  10. // Disposing the encoder will complete the file.
  11. // Uses default .NET GIF encoding and adds animation headers.
  12. // Adapted from <see cref="http://github.com/DataDink/Bumpkit/blob/master/BumpKit/BumpKit/GifEncoder.cs"/>
  13. // </remarks>
  14. // </summary>
  15. // --------------------------------------------------------------------------------------------------------------------
  16. namespace ImageProcessor.Imaging
  17. {
  18. #region Using
  19. using System;
  20. using System.Drawing.Imaging;
  21. using System.IO;
  22. using System.Linq;
  23. #endregion
  24. /// <summary>
  25. /// Encodes multiple images as an animated gif to a stream.
  26. /// <remarks>
  27. /// Always wire this up in a using block.
  28. /// Disposing the encoder will complete the file.
  29. /// Uses default .NET GIF encoding and adds animation headers.
  30. /// Adapted from <see cref="http://github.com/DataDink/Bumpkit/blob/master/BumpKit/BumpKit/GifEncoder.cs"/>
  31. /// </remarks>
  32. /// </summary>
  33. public class GifEncoder : IDisposable
  34. {
  35. #region Constants
  36. /// <summary>
  37. /// The application block size.
  38. /// </summary>
  39. private const byte ApplicationBlockSize = 0x0b;
  40. /// <summary>
  41. /// The application extension block identifier.
  42. /// </summary>
  43. private const int ApplicationExtensionBlockIdentifier = 0xff21;
  44. /// <summary>
  45. /// The application identification.
  46. /// </summary>
  47. private const string ApplicationIdentification = "NETSCAPE2.0";
  48. /// <summary>
  49. /// The file trailer.
  50. /// </summary>
  51. private const byte FileTrailer = 0x3b;
  52. /// <summary>
  53. /// The file type.
  54. /// </summary>
  55. private const string FileType = "GIF";
  56. /// <summary>
  57. /// The file version.
  58. /// </summary>
  59. private const string FileVersion = "89a";
  60. /// <summary>
  61. /// The graphic control extension block identifier.
  62. /// </summary>
  63. private const int GraphicControlExtensionBlockIdentifier = 0xf921;
  64. /// <summary>
  65. /// The graphic control extension block size.
  66. /// </summary>
  67. private const byte GraphicControlExtensionBlockSize = 0x04;
  68. /// <summary>
  69. /// The source color block length.
  70. /// </summary>
  71. private const long SourceColorBlockLength = 768;
  72. /// <summary>
  73. /// The source color block position.
  74. /// </summary>
  75. private const long SourceColorBlockPosition = 13;
  76. /// <summary>
  77. /// The source global color info position.
  78. /// </summary>
  79. private const long SourceGlobalColorInfoPosition = 10;
  80. /// <summary>
  81. /// The source graphic control extension length.
  82. /// </summary>
  83. private const long SourceGraphicControlExtensionLength = 8;
  84. /// <summary>
  85. /// The source graphic control extension position.
  86. /// </summary>
  87. private const long SourceGraphicControlExtensionPosition = 781;
  88. /// <summary>
  89. /// The source image block header length.
  90. /// </summary>
  91. private const long SourceImageBlockHeaderLength = 11;
  92. /// <summary>
  93. /// The source image block position.
  94. /// </summary>
  95. private const long SourceImageBlockPosition = 789;
  96. #endregion
  97. #region Fields
  98. /// <summary>
  99. /// The stream.
  100. /// </summary>
  101. // ReSharper disable once FieldCanBeMadeReadOnly.Local
  102. private MemoryStream inputStream;
  103. /// <summary>
  104. /// The height.
  105. /// </summary>
  106. private int? height;
  107. /// <summary>
  108. /// A value indicating whether this instance of the given entity has been disposed.
  109. /// </summary>
  110. /// <value><see langword="true"/> if this instance has been disposed; otherwise, <see langword="false"/>.</value>
  111. /// <remarks>
  112. /// If the entity is disposed, it must not be disposed a second
  113. /// time. The isDisposed field is set the first time the entity
  114. /// is disposed. If the isDisposed field is true, then the Dispose()
  115. /// method will not dispose again. This help not to prolong the entity's
  116. /// life in the Garbage Collector.
  117. /// </remarks>
  118. private bool isDisposed;
  119. /// <summary>
  120. /// The is first image.
  121. /// </summary>
  122. private bool isFirstImage = true;
  123. /// <summary>
  124. /// The repeat count.
  125. /// </summary>
  126. private int? repeatCount;
  127. /// <summary>
  128. /// The width.
  129. /// </summary>
  130. private int? width;
  131. #endregion
  132. #region Constructors
  133. /// <summary>
  134. /// Initializes a new instance of the <see cref="GifEncoder"/> class.
  135. /// </summary>
  136. /// <param name="stream">
  137. /// The stream that will be written to.
  138. /// </param>
  139. /// <param name="width">
  140. /// Sets the width for this gif or null to use the first frame's width.
  141. /// </param>
  142. /// <param name="height">
  143. /// Sets the height for this gif or null to use the first frame's height.
  144. /// </param>
  145. /// <param name="repeatCount">
  146. /// The number of times to repeat the animation.
  147. /// </param>
  148. public GifEncoder(MemoryStream stream, int? width = null, int? height = null, int? repeatCount = null)
  149. {
  150. this.inputStream = stream;
  151. this.width = width;
  152. this.height = height;
  153. this.repeatCount = repeatCount;
  154. }
  155. /// <summary>
  156. /// Finalizes an instance of the <see cref="GifEncoder"/> class.
  157. /// </summary>
  158. /// <remarks>
  159. /// Use C# destructor syntax for finalization code.
  160. /// This destructor will run only if the Dispose method
  161. /// does not get called.
  162. /// It gives your base class the opportunity to finalize.
  163. /// Do not provide destructors in types derived from this class.
  164. /// </remarks>
  165. ~GifEncoder()
  166. {
  167. // Do not re-create Dispose clean-up code here.
  168. // Calling Dispose(false) is optimal in terms of
  169. // readability and maintainability.
  170. this.Dispose(false);
  171. }
  172. #endregion
  173. #region Properties
  174. /// <summary>
  175. /// Gets or sets the frame delay.
  176. /// </summary>
  177. public TimeSpan FrameDelay { get; set; }
  178. #endregion
  179. #region Public Methods and Operators
  180. /// <summary>
  181. /// Adds a frame to the gif.
  182. /// </summary>
  183. /// <param name="frame">
  184. /// The <see cref="GifFrame"/> containing the image.
  185. /// </param>
  186. public void AddFrame(GifFrame frame)
  187. {
  188. using (MemoryStream gifStream = new MemoryStream())
  189. {
  190. frame.Image.Save(gifStream, ImageFormat.Gif);
  191. if (this.isFirstImage)
  192. {
  193. // Steal the global color table info
  194. this.WriteHeaderBlock(gifStream, frame.Image.Width, frame.Image.Height);
  195. }
  196. this.WriteGraphicControlBlock(gifStream, frame.Delay);
  197. this.WriteImageBlock(gifStream, !this.isFirstImage, frame.X, frame.Y, frame.Image.Width, frame.Image.Height);
  198. }
  199. this.isFirstImage = false;
  200. }
  201. /// <summary>
  202. /// Disposes the object and frees resources for the Garbage Collector.
  203. /// </summary>
  204. public void Dispose()
  205. {
  206. this.Dispose(true);
  207. // This object will be cleaned up by the Dispose method.
  208. // Therefore, you should call GC.SupressFinalize to
  209. // take this object off the finalization queue
  210. // and prevent finalization code for this object
  211. // from executing a second time.
  212. GC.SuppressFinalize(this);
  213. }
  214. #endregion
  215. #region Methods
  216. /// <summary>
  217. /// Disposes the object and frees resources for the Garbage Collector.
  218. /// </summary>
  219. /// <param name="disposing">
  220. /// If true, the object gets disposed.
  221. /// </param>
  222. protected virtual void Dispose(bool disposing)
  223. {
  224. if (this.isDisposed)
  225. {
  226. return;
  227. }
  228. if (disposing)
  229. {
  230. // Complete Application Block
  231. this.WriteByte(0);
  232. // Complete File
  233. this.WriteByte(FileTrailer);
  234. // Push the data
  235. this.inputStream.Flush();
  236. }
  237. // Call the appropriate methods to clean up
  238. // unmanaged resources here.
  239. // Note disposing is done.
  240. this.isDisposed = true;
  241. }
  242. /// <summary>
  243. /// Writes the header block of the animated gif to the stream.
  244. /// </summary>
  245. /// <param name="sourceGif">
  246. /// The source gif.
  247. /// </param>
  248. /// <param name="w">
  249. /// The width of the image.
  250. /// </param>
  251. /// <param name="h">
  252. /// The height of the image.
  253. /// </param>
  254. private void WriteHeaderBlock(Stream sourceGif, int w, int h)
  255. {
  256. int count = this.repeatCount.GetValueOrDefault(0);
  257. // File Header
  258. this.WriteString(FileType);
  259. this.WriteString(FileVersion);
  260. this.WriteShort(this.width.GetValueOrDefault(w)); // Initial Logical Width
  261. this.WriteShort(this.height.GetValueOrDefault(h)); // Initial Logical Height
  262. sourceGif.Position = SourceGlobalColorInfoPosition;
  263. this.WriteByte(sourceGif.ReadByte()); // Global Color Table Info
  264. this.WriteByte(0); // Background Color Index
  265. this.WriteByte(0); // Pixel aspect ratio
  266. this.WriteColorTable(sourceGif);
  267. // The different browsers interpret the spec differently when adding a loop.
  268. // If the loop count is one IE and FF &lt; 3 (incorrectly) loop an extra number of times.
  269. // Removing the Netscape header should fix this.
  270. if (count != 1)
  271. {
  272. // Application Extension Header
  273. this.WriteShort(ApplicationExtensionBlockIdentifier);
  274. this.WriteByte(ApplicationBlockSize);
  275. this.WriteString(ApplicationIdentification);
  276. this.WriteByte(3); // Application block length
  277. this.WriteByte(1);
  278. this.WriteShort(count); // Repeat count for images.
  279. this.WriteByte(0); // Terminator
  280. }
  281. }
  282. /// <summary>
  283. /// The write byte.
  284. /// </summary>
  285. /// <param name="value">
  286. /// The value.
  287. /// </param>
  288. private void WriteByte(int value)
  289. {
  290. this.inputStream.WriteByte(Convert.ToByte(value));
  291. }
  292. /// <summary>
  293. /// The write color table.
  294. /// </summary>
  295. /// <param name="sourceGif">
  296. /// The source gif.
  297. /// </param>
  298. private void WriteColorTable(Stream sourceGif)
  299. {
  300. sourceGif.Position = SourceColorBlockPosition; // Locating the image color table
  301. byte[] colorTable = new byte[SourceColorBlockLength];
  302. sourceGif.Read(colorTable, 0, colorTable.Length);
  303. this.inputStream.Write(colorTable, 0, colorTable.Length);
  304. }
  305. /// <summary>
  306. /// The write graphic control block.
  307. /// </summary>
  308. /// <param name="sourceGif">
  309. /// The source gif.
  310. /// </param>
  311. /// <param name="frameDelay">
  312. /// The frame delay.
  313. /// </param>
  314. private void WriteGraphicControlBlock(Stream sourceGif, int frameDelay)
  315. {
  316. sourceGif.Position = SourceGraphicControlExtensionPosition; // Locating the source GCE
  317. byte[] blockhead = new byte[SourceGraphicControlExtensionLength];
  318. sourceGif.Read(blockhead, 0, blockhead.Length); // Reading source GCE
  319. this.WriteShort(GraphicControlExtensionBlockIdentifier); // Identifier
  320. this.WriteByte(GraphicControlExtensionBlockSize); // Block Size
  321. this.WriteByte(blockhead[3] & 0xf7 | 0x08); // Setting disposal flag
  322. this.WriteShort(Convert.ToInt32(frameDelay / 10)); // Setting frame delay
  323. this.WriteByte(blockhead[6]); // Transparent color index
  324. this.WriteByte(0); // Terminator
  325. }
  326. /// <summary>
  327. /// The write image block.
  328. /// </summary>
  329. /// <param name="sourceGif">
  330. /// The source gif.
  331. /// </param>
  332. /// <param name="includeColorTable">
  333. /// The include color table.
  334. /// </param>
  335. /// <param name="x">
  336. /// The x position to write the image block.
  337. /// </param>
  338. /// <param name="y">
  339. /// The y position to write the image block.
  340. /// </param>
  341. /// <param name="h">
  342. /// The height of the image block.
  343. /// </param>
  344. /// <param name="w">
  345. /// The width of the image block.
  346. /// </param>
  347. private void WriteImageBlock(Stream sourceGif, bool includeColorTable, int x, int y, int h, int w)
  348. {
  349. sourceGif.Position = SourceImageBlockPosition; // Locating the image block
  350. byte[] header = new byte[SourceImageBlockHeaderLength];
  351. sourceGif.Read(header, 0, header.Length);
  352. this.WriteByte(header[0]); // Separator
  353. this.WriteShort(x); // Position X
  354. this.WriteShort(y); // Position Y
  355. this.WriteShort(h); // Height
  356. this.WriteShort(w); // Width
  357. if (includeColorTable)
  358. {
  359. // If first frame, use global color table - else use local
  360. sourceGif.Position = SourceGlobalColorInfoPosition;
  361. this.WriteByte(sourceGif.ReadByte() & 0x3f | 0x80); // Enabling local color table
  362. this.WriteColorTable(sourceGif);
  363. }
  364. else
  365. {
  366. this.WriteByte(header[9] & 0x07 | 0x07); // Disabling local color table
  367. }
  368. this.WriteByte(header[10]); // LZW Min Code Size
  369. // Read/Write image data
  370. sourceGif.Position = SourceImageBlockPosition + SourceImageBlockHeaderLength;
  371. int dataLength = sourceGif.ReadByte();
  372. while (dataLength > 0)
  373. {
  374. byte[] imgData = new byte[dataLength];
  375. sourceGif.Read(imgData, 0, dataLength);
  376. this.inputStream.WriteByte(Convert.ToByte(dataLength));
  377. this.inputStream.Write(imgData, 0, dataLength);
  378. dataLength = sourceGif.ReadByte();
  379. }
  380. this.inputStream.WriteByte(0); // Terminator
  381. }
  382. /// <summary>
  383. /// The write short.
  384. /// </summary>
  385. /// <param name="value">
  386. /// The value.
  387. /// </param>
  388. private void WriteShort(int value)
  389. {
  390. this.inputStream.WriteByte(Convert.ToByte(value & 0xff));
  391. this.inputStream.WriteByte(Convert.ToByte((value >> 8) & 0xff));
  392. }
  393. /// <summary>
  394. /// The write string.
  395. /// </summary>
  396. /// <param name="value">
  397. /// The value.
  398. /// </param>
  399. private void WriteString(string value)
  400. {
  401. this.inputStream.Write(value.ToArray().Select(c => (byte)c).ToArray(), 0, value.Length);
  402. }
  403. #endregion
  404. }
  405. }