// // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // namespace ImageProcessor.Formats { using System; using System.IO; using System.Linq; /// /// Image encoder for writing image data to a stream in gif format. /// public class GifEncoder : IImageEncoder { /// /// The the transparency threshold. /// private int threshold = 128; /// /// Gets or sets the quality of output for images. /// /// For gifs the value ranges from 1 to 256. public int Quality { get; set; } /// public string Extension => "gif"; /// public string MimeType => "image/gif"; /// /// Gets or sets the transparency threshold. /// public int Threshold { get { return this.threshold; } set { this.threshold = value.Clamp(0, 255); } } /// public bool IsSupportedFileExtension(string extension) { Guard.NotNullOrEmpty(extension, nameof(extension)); extension = extension.StartsWith(".") ? extension.Substring(1) : extension; return extension.Equals(this.Extension, StringComparison.OrdinalIgnoreCase); } /// public void Encode(ImageBase imageBase, Stream stream) { Guard.NotNull(imageBase, nameof(imageBase)); Guard.NotNull(stream, nameof(stream)); Image image = (Image)imageBase; // Write the header. // File Header signature and version. this.WriteString(stream, GifConstants.FileType); this.WriteString(stream, GifConstants.FileVersion); // Calculate the quality. int quality = this.Quality > 0 ? this.Quality : imageBase.Quality; quality = quality > 0 ? quality.Clamp(1, 256) : 256; // Get the number of bits. int bitDepth = this.GetBitsNeededForColorDepth(quality); // Write the LSD and check to see if we need a global color table. // Always true just now. this.WriteGlobalLogicalScreenDescriptor(image, stream, bitDepth); QuantizedImage quantized = this.WriteColorTable(imageBase, stream, quality, bitDepth); this.WriteGraphicalControlExtension(imageBase, stream); this.WriteImageDescriptor(quantized, quality, stream); if (image.Frames.Any()) { this.WriteApplicationExtension(stream, image.RepeatCount, image.Frames.Count); foreach (ImageFrame frame in image.Frames) { this.WriteGraphicalControlExtension(frame, stream); this.WriteFrameImageDescriptor(frame, stream); } } // TODO: Write Comments extension etc this.WriteByte(stream, GifConstants.EndIntroducer); } /// /// Writes the logical screen descriptor to the stream. /// /// The image to encode. /// The stream to write to. /// The bit depth. /// The private bool WriteGlobalLogicalScreenDescriptor(Image image, Stream stream, int bitDepth) { Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); GifLogicalScreenDescriptor descriptor = new GifLogicalScreenDescriptor { Width = (short)image.Width, Height = (short)image.Height, GlobalColorTableFlag = true, GlobalColorTableSize = bitDepth }; this.WriteShort(stream, descriptor.Width); this.WriteShort(stream, descriptor.Height); int packed = 0x80 | // 1 : Global color table flag = 1 (GCT used) bitDepth - 1 | // 2-4 : color resolution 0x00 | // 5 : GCT sort flag = 0 bitDepth - 1; // 6-8 : GCT size TODO: Check this. this.WriteByte(stream, packed); this.WriteByte(stream, descriptor.BackgroundColorIndex); // Background Color Index this.WriteByte(stream, descriptor.PixelAspectRatio); // Pixel aspect ratio. Assume 1:1 return descriptor.GlobalColorTableFlag; } /// /// Writes the color table to the stream. /// /// The to encode. /// The stream to write to. /// The quality (number of colors) to encode the image to. /// The bit depth. /// The private QuantizedImage WriteColorTable(ImageBase image, Stream stream, int quality, int bitDepth) { // Quantize the image returning a pallete. IQuantizer quantizer = new OctreeQuantizer(quality.Clamp(1, 255), bitDepth) {Threshold = this.threshold}; QuantizedImage quantizedImage = quantizer.Quantize(image); // Grab the pallete and write it to the stream. Bgra32[] pallete = quantizedImage.Palette; int pixelCount = pallete.Length; // 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++) { int offset = i * 3; Bgra32 color = pallete[i]; colorTable[offset + 2] = color.B; colorTable[offset + 1] = color.G; colorTable[offset + 0] = color.R; } stream.Write(colorTable, 0, colorTableLength); return quantizedImage; } /// /// Writes the graphics control extension to the stream. /// /// The to encode. /// The stream to write to. private void WriteGraphicalControlExtension(ImageBase image, Stream stream) { // Calculate the quality. int quality = this.Quality > 0 ? this.Quality : image.Quality; quality = quality > 0 ? quality.Clamp(1, 256) : 256; // TODO: Check transparency logic. bool hasTransparent = quality > 1; DisposalMethod disposalMethod = hasTransparent ? DisposalMethod.RestoreToBackground : DisposalMethod.Unspecified; 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); this.WriteByte(stream, 4); // Size int packed = 0 | // 1-3 : Reserved (int)extension.DisposalMethod << 2 | // 4-6 : Disposal 0 | // 7 : User input - 0 = none (extension.TransparencyFlag ? 1 : 0); // 8: Has transparent. this.WriteByte(stream, packed); this.WriteShort(stream, extension.DelayTime); this.WriteByte(stream, extension.TransparencyIndex); this.WriteByte(stream, GifConstants.Terminator); } /// /// Writes the application exstension to the stream. /// /// The stream to write to. /// The animated image repeat count. /// Th number of image frames. private void WriteApplicationExtension(Stream stream, ushort repeatCount, int frames) { // Application Extension Header if (repeatCount != 1 && frames > 0) { // 0 means loop indefinitely. count is set as play n + 1 times. // TODO: Check this as the correct value might be pulled from the decoder. repeatCount = (ushort)Math.Max(0, repeatCount - 1); this.WriteByte(stream, GifConstants.ExtensionIntroducer); // NETSCAPE2.0 this.WriteByte(stream, GifConstants.ApplicationExtensionLabel); this.WriteByte(stream, GifConstants.ApplicationBlockSize); this.WriteString(stream, GifConstants.ApplicationIdentification); this.WriteByte(stream, 3); // Application block length this.WriteByte(stream, 1); // Data sub-block index (always 1) this.WriteShort(stream, repeatCount); // Repeat count for images. this.WriteByte(stream, GifConstants.Terminator); // Terminator } } /// /// Writes the image descriptor to the stream. /// /// The containing indexed pixels. /// The quality (number of colors) to encode the image to. /// The stream to write to. private void WriteImageDescriptor(QuantizedImage image, int quality, 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 bitDepth = this.GetBitsNeededForColorDepth(quality); // No LCT use GCT. this.WriteByte(stream, 0); // Write the image data. this.WriteImageData(image, stream, bitDepth); } /// /// Writes the image descriptor to the stream. /// /// The to be encoded. /// The stream to write to. 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); } /// /// Writes the image pixel data to the stream. /// /// The containing indexed pixels. /// The stream to write to. /// The bit depth of the image. private void WriteImageData(QuantizedImage image, Stream stream, int bitDepth) { byte[] indexedPixels = image.Pixels; LzwEncoder encoder = new LzwEncoder(indexedPixels, (byte)bitDepth); encoder.Encode(stream); this.WriteByte(stream, GifConstants.Terminator); } /// /// Writes a short to the given stream. /// /// The containing image data. /// The value to write. private void WriteShort(Stream stream, int value) { // Leave only one significant byte. stream.WriteByte(Convert.ToByte(value & 0xff)); stream.WriteByte(Convert.ToByte((value >> 8) & 0xff)); } /// /// Writes a byte to the given stream. /// /// The containing image data. /// The value to write. private void WriteByte(Stream stream, int value) { stream.WriteByte(Convert.ToByte(value)); } /// /// Writes a string to the given stream. /// /// The containing image data. /// The value to write. private void WriteString(Stream stream, string value) { char[] chars = value.ToCharArray(); foreach (char c in chars) { stream.WriteByte((byte)c); } } /// /// Returns how many bits are required to store the specified number of colors. /// Performs a Log2() on the value. /// /// The number of colors. /// /// The /// private int GetBitsNeededForColorDepth(int colors) { return (int)Math.Ceiling(Math.Log(colors, 2)); } } }