// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; using System.IO; using System.Linq; using System.Text; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.MetaData; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Quantization; namespace SixLabors.ImageSharp.Formats.Gif { /// /// Performs the gif encoding operation. /// internal sealed class GifEncoderCore { private readonly MemoryManager memoryManager; /// /// The temp buffer used to reduce allocations. /// private readonly byte[] buffer = new byte[16]; /// /// Gets the TextEncoding /// private readonly Encoding textEncoding; /// /// Gets or sets the quantizer for reducing the color count. /// private readonly IQuantizer quantizer; /// /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded. /// private readonly bool ignoreMetadata; /// /// The number of bits requires to store the image palette. /// private int bitDepth; /// /// Whether the current image has multiple frames. /// private bool hasFrames; /// /// Initializes a new instance of the class. /// /// The to use for buffer allocations. /// The options for the encoder. public GifEncoderCore(MemoryManager memoryManager, IGifEncoderOptions options) { this.memoryManager = memoryManager; this.textEncoding = options.TextEncoding ?? GifConstants.DefaultEncoding; this.quantizer = options.Quantizer; this.ignoreMetadata = options.IgnoreMetadata; } /// /// Encodes the image to the specified stream from the . /// /// The pixel format. /// The to encode from. /// The to encode the image data to. public void Encode(Image image, Stream stream) where TPixel : struct, IPixel { Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); // Do not use IDisposable pattern here as we want to preserve the stream. var writer = new EndianBinaryWriter(Endianness.LittleEndian, stream); this.hasFrames = image.Frames.Count > 1; // Quantize the image returning a palette. QuantizedFrame quantized = this.quantizer.CreateFrameQuantizer().QuantizeFrame(image.Frames.RootFrame); // Get the number of bits. this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8); int index = this.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.WriteComments(image, writer); // Write additional frames. if (this.hasFrames) { this.WriteApplicationExtension(writer, image.MetaData.RepeatCount, image.Frames.Count); } foreach (ImageFrame frame in image.Frames) { if (quantized == null) { quantized = this.quantizer.CreateFrameQuantizer().QuantizeFrame(frame); } this.WriteGraphicalControlExtension(frame.MetaData, writer, this.GetTransparentIndex(quantized)); this.WriteImageDescriptor(frame, writer); this.WriteColorTable(quantized, writer); this.WriteImageData(quantized, writer); quantized = null; // So next frame can regenerate it } // TODO: Write extension etc writer.Write(GifConstants.EndIntroducer); } /// /// Returns the index of the most transparent color in the palette. /// /// /// The quantized. /// /// The pixel format. /// /// The . /// private int GetTransparentIndex(QuantizedFrame quantized) where TPixel : struct, IPixel { // Transparent pixels are much more likely to be found at the end of a palette int index = -1; var trans = default(Rgba32); for (int i = quantized.Palette.Length - 1; i >= 0; i--) { quantized.Palette[i].ToRgba32(ref trans); if (trans.Equals(default(Rgba32))) { 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 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 transparencyIndex) where TPixel : struct, IPixel { var descriptor = new GifLogicalScreenDescriptor { Width = (short)image.Width, Height = (short)image.Height, GlobalColorTableFlag = false, // TODO: Always false for now. GlobalColorTableSize = this.bitDepth - 1, BackgroundColorIndex = unchecked((byte)transparencyIndex) }; writer.Write((ushort)descriptor.Width); writer.Write((ushort)descriptor.Height); var 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 this.buffer[0] = field.Byte; this.buffer[1] = descriptor.BackgroundColorIndex; // Background Color Index this.buffer[2] = descriptor.PixelAspectRatio; // Pixel aspect ratio. Assume 1:1 writer.Write(this.buffer, 0, 3); } /// /// 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) { this.buffer[0] = GifConstants.ExtensionIntroducer; this.buffer[1] = GifConstants.ApplicationExtensionLabel; this.buffer[2] = GifConstants.ApplicationBlockSize; writer.Write(this.buffer, 0, 3); 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(0, repeatCount - 1); writer.Write(repeatCount); // Repeat count for images. writer.Write(GifConstants.Terminator); // Terminator } } /// /// Writes the image comments to the stream. /// /// The pixel format. /// The to be encoded. /// The stream to write to. private void WriteComments(Image image, EndianBinaryWriter writer) where TPixel : struct, IPixel { if (this.ignoreMetadata) { return; } ImageProperty property = image.MetaData.Properties.FirstOrDefault(p => p.Name == GifConstants.Comments); if (property == null || string.IsNullOrEmpty(property.Value)) { return; } byte[] comments = this.textEncoding.GetBytes(property.Value); int count = Math.Min(comments.Length, 255); this.buffer[0] = GifConstants.ExtensionIntroducer; this.buffer[1] = GifConstants.CommentLabel; this.buffer[2] = (byte)count; writer.Write(this.buffer, 0, 3); writer.Write(comments, 0, count); writer.Write(GifConstants.Terminator); } /// /// Writes the graphics control extension to the stream. /// /// The metadata of the image or frame. /// The stream to write to. /// The index of the color in the color palette to make transparent. private void WriteGraphicalControlExtension(ImageFrameMetaData metaData, EndianBinaryWriter writer, int transparencyIndex) { var extension = new GifGraphicsControlExtension { DisposalMethod = metaData.DisposalMethod, TransparencyFlag = transparencyIndex > -1, TransparencyIndex = unchecked((byte)transparencyIndex), DelayTime = metaData.FrameDelay }; // Write the intro. this.buffer[0] = GifConstants.ExtensionIntroducer; this.buffer[1] = GifConstants.GraphicControlLabel; this.buffer[2] = 4; writer.Write(this.buffer, 0, 3); var 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(extension.TransparencyIndex); writer.Write(GifConstants.Terminator); } /// /// Writes the image descriptor to the stream. /// /// The pixel format. /// The to be encoded. /// The stream to write to. private void WriteImageDescriptor(ImageFrame image, EndianBinaryWriter writer) where TPixel : struct, IPixel { 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); var 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 to encode. /// The writer to write to the stream with. private void WriteColorTable(QuantizedFrame image, EndianBinaryWriter writer) where TPixel : struct, IPixel { // Grab the palette and write it to the stream. int pixelCount = image.Palette.Length; // Get max colors for bit depth. int colorTableLength = (int)Math.Pow(2, this.bitDepth) * 3; var rgb = default(Rgb24); using (IManagedByteBuffer colorTable = this.memoryManager.AllocateManagedByteBuffer(colorTableLength)) { Span colorTableSpan = colorTable.Span; for (int i = 0; i < pixelCount; i++) { int offset = i * 3; image.Palette[i].ToRgb24(ref rgb); colorTableSpan[offset] = rgb.R; colorTableSpan[offset + 1] = rgb.G; colorTableSpan[offset + 2] = rgb.B; } writer.Write(colorTable.Array, 0, colorTableLength); } } /// /// Writes the image pixel data to the stream. /// /// The pixel format. /// The containing indexed pixels. /// The stream to write to. private void WriteImageData(QuantizedFrame image, EndianBinaryWriter writer) where TPixel : struct, IPixel { using (var encoder = new LzwEncoder(this.memoryManager, image.Pixels, (byte)this.bitDepth)) { encoder.Encode(writer.BaseStream); } } } }