// 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);
}
}
}
}