// // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // namespace ImageSharp.Formats { using System; using System.IO; using System.Linq; using System.Threading.Tasks; using IO; using Quantizers; /// /// Performs the gif encoding operation. /// internal sealed class GifEncoderCore { /// /// The number of bits requires to store the image palette. /// private int bitDepth; /// /// Gets or sets the quality of output for images. /// /// For gifs the value ranges from 1 to 256. public int Quality { get; set; } /// /// Gets or sets the transparency threshold. /// public byte Threshold { get; set; } = 128; /// /// Gets or sets the quantizer for reducing the color count. /// public IQuantizer Quantizer { get; set; } /// /// Encodes the image to the specified stream from the . /// /// The pixel format. /// The packed format. uint, long, float. /// The to encode from. /// The to encode the image data to. public void Encode(Image image, Stream stream) where TColor : struct, IPackedPixel where TPacked : struct { Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); if (this.Quantizer == null) { this.Quantizer = new OctreeQuantizer(); } // Do not use IDisposable pattern here as we want to preserve the stream. EndianBinaryWriter writer = new EndianBinaryWriter(EndianBitConverter.Little, stream); // Ensure that quality can be set but has a fallback. int quality = this.Quality > 0 ? this.Quality : image.Quality; this.Quality = quality > 0 ? quality.Clamp(1, 256) : 256; // Get the number of bits. this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(this.Quality); // Quantize the image returning a palette. QuantizedImage quantized = ((IQuantizer)this.Quantizer).Quantize(image, this.Quality); int index = GetTransparentIndex(quantized); // Write the header. this.WriteHeader(writer); // Write the LSD. We'll use local color tables for now. this.WriteLogicalScreenDescriptor(image, writer, index); // Write the first frame. this.WriteGraphicalControlExtension(image, writer, index); this.WriteImageDescriptor(image, writer); this.WriteColorTable(quantized, writer); this.WriteImageData(quantized, writer); // Write additional frames. if (image.Frames.Any()) { this.WriteApplicationExtension(writer, image.RepeatCount, image.Frames.Count); foreach (ImageFrame frame in image.Frames) { QuantizedImage quantizedFrame = ((IQuantizer)this.Quantizer).Quantize(frame, this.Quality); this.WriteGraphicalControlExtension(frame, writer, GetTransparentIndex(quantizedFrame)); this.WriteImageDescriptor(frame, writer); this.WriteColorTable(quantizedFrame, writer); this.WriteImageData(quantizedFrame, writer); } } // TODO: Write Comments extension etc writer.Write(GifConstants.EndIntroducer); } /// /// Returns the index of the most transparent color in the palette. /// /// /// The quantized. /// /// The pixel format. /// The packed format. uint, long, float. /// /// The . /// private static int GetTransparentIndex(QuantizedImage quantized) where TColor : struct, IPackedPixel where TPacked : struct { // Find the lowest alpha value and make it the transparent index. int index = 255; float alpha = 1; for (int i = 0; i < quantized.Palette.Length; i++) { float a = quantized.Palette[i].ToVector4().W; if (a < alpha) { alpha = a; index = i; } } return index; } /// /// Writes the file header signature and version to the stream. /// /// The writer to write to the stream with. private void WriteHeader(EndianBinaryWriter writer) { writer.Write((GifConstants.FileType + GifConstants.FileVersion).ToCharArray()); } /// /// Writes the logical screen descriptor to the stream. /// /// The pixel format. /// The packed format. uint, long, float. /// The image to encode. /// The writer to write to the stream with. /// The transparency index to set the default background index to. private void WriteLogicalScreenDescriptor(Image image, EndianBinaryWriter writer, int tranparencyIndex) where TColor : struct, IPackedPixel where TPacked : struct { GifLogicalScreenDescriptor descriptor = new GifLogicalScreenDescriptor { Width = (short)image.Width, Height = (short)image.Height, GlobalColorTableFlag = false, // Always false for now. GlobalColorTableSize = this.bitDepth - 1, BackgroundColorIndex = (byte)(tranparencyIndex > -1 ? tranparencyIndex : 255) }; writer.Write((ushort)descriptor.Width); writer.Write((ushort)descriptor.Height); PackedField field = default(PackedField); field.SetBit(0, descriptor.GlobalColorTableFlag); // 1 : Global color table flag = 1 || 0 (GCT used/ not used) field.SetBits(1, 3, descriptor.GlobalColorTableSize); // 2-4 : color resolution field.SetBit(4, false); // 5 : GCT sort flag = 0 field.SetBits(5, 3, descriptor.GlobalColorTableSize); // 6-8 : GCT size. 2^(N+1) // Reduce the number of writes byte[] arr = { field.Byte, descriptor.BackgroundColorIndex, // Background Color Index descriptor.PixelAspectRatio // Pixel aspect ratio. Assume 1:1 }; writer.Write(arr); } /// /// Writes the application extension to the stream. /// /// The writer to write to the stream with. /// The animated image repeat count. /// The number of image frames. private void WriteApplicationExtension(EndianBinaryWriter writer, ushort repeatCount, int frames) { // Application Extension Header if (repeatCount != 1 && frames > 0) { byte[] ext = { GifConstants.ExtensionIntroducer, GifConstants.ApplicationExtensionLabel, GifConstants.ApplicationBlockSize }; writer.Write(ext); writer.Write(GifConstants.ApplicationIdentification.ToCharArray()); // NETSCAPE2.0 writer.Write((byte)3); // Application block length writer.Write((byte)1); // Data sub-block index (always 1) // 0 means loop indefinitely. Count is set as play n + 1 times. repeatCount = (ushort)(Math.Max((ushort)0, repeatCount) - 1); writer.Write(repeatCount); // Repeat count for images. writer.Write(GifConstants.Terminator); // Terminator } } /// /// Writes the graphics control extension to the stream. /// /// The pixel format. /// The packed format. uint, long, float. /// The to encode. /// The stream to write to. /// The index of the color in the color palette to make transparent. private void WriteGraphicalControlExtension(ImageBase image, EndianBinaryWriter writer, int transparencyIndex) where TColor : struct, IPackedPixel where TPacked : struct { // TODO: Check transparency logic. bool hasTransparent = transparencyIndex > -1; DisposalMethod disposalMethod = hasTransparent ? DisposalMethod.RestoreToBackground : DisposalMethod.Unspecified; GifGraphicsControlExtension extension = new GifGraphicsControlExtension() { DisposalMethod = disposalMethod, TransparencyFlag = hasTransparent, TransparencyIndex = transparencyIndex, DelayTime = image.FrameDelay }; // Reduce the number of writes. byte[] intro = { GifConstants.ExtensionIntroducer, GifConstants.GraphicControlLabel, 4 // Size }; writer.Write(intro); PackedField field = default(PackedField); field.SetBits(3, 3, (int)extension.DisposalMethod); // 1-3 : Reserved, 4-6 : Disposal // TODO: Allow this as an option. field.SetBit(6, false); // 7 : User input - 0 = none field.SetBit(7, extension.TransparencyFlag); // 8: Has transparent. writer.Write(field.Byte); writer.Write((ushort)extension.DelayTime); writer.Write((byte)(extension.TransparencyIndex == -1 ? 255 : extension.TransparencyIndex)); writer.Write(GifConstants.Terminator); } /// /// Writes the image descriptor to the stream. /// /// The pixel format. /// The packed format. uint, long, float. /// The to be encoded. /// The stream to write to. private void WriteImageDescriptor(ImageBase image, EndianBinaryWriter writer) where TColor : struct, IPackedPixel where TPacked : struct { writer.Write(GifConstants.ImageDescriptorLabel); // 2c // TODO: Can we capture this? writer.Write((ushort)0); // Left position writer.Write((ushort)0); // Top position writer.Write((ushort)image.Width); writer.Write((ushort)image.Height); PackedField field = default(PackedField); field.SetBit(0, true); // 1: Local color table flag = 1 (LCT used) field.SetBit(1, false); // 2: Interlace flag 0 field.SetBit(2, false); // 3: Sort flag 0 field.SetBits(5, 3, this.bitDepth - 1); // 4-5: Reserved, 6-8 : LCT size. 2^(N+1) writer.Write(field.Byte); } /// /// Writes the color table to the stream. /// /// The pixel format. /// The packed format. uint, long, float. /// The to encode. /// The writer to write to the stream with. private void WriteColorTable(QuantizedImage image, EndianBinaryWriter writer) where TColor : struct, IPackedPixel where TPacked : struct { // Grab the palette and write it to the stream. TColor[] palette = image.Palette; int pixelCount = palette.Length; // Get max colors for bit depth. int colorTableLength = (int)Math.Pow(2, this.bitDepth) * 3; byte[] colorTable = new byte[colorTableLength]; Parallel.For( 0, pixelCount, i => { int offset = i * 3; Color color = new Color(palette[i].ToVector4()); colorTable[offset] = color.R; colorTable[offset + 1] = color.G; colorTable[offset + 2] = color.B; }); writer.Write(colorTable, 0, colorTableLength); } /// /// Writes the image pixel data to the stream. /// /// The pixel format. /// The packed format. uint, long, float. /// The containing indexed pixels. /// The stream to write to. private void WriteImageData(QuantizedImage image, EndianBinaryWriter writer) where TColor : struct, IPackedPixel where TPacked : struct { byte[] indexedPixels = image.Pixels; LzwEncoder encoder = new LzwEncoder(indexedPixels, (byte)this.bitDepth); encoder.Encode(writer.BaseStream); } } }