📷 A modern, cross-platform, 2D Graphics library for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

369 lines
15 KiB

// 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
{
/// <summary>
/// Performs the gif encoding operation.
/// </summary>
internal sealed class GifEncoderCore
{
private readonly MemoryManager memoryManager;
/// <summary>
/// The temp buffer used to reduce allocations.
/// </summary>
private readonly byte[] buffer = new byte[16];
/// <summary>
/// Gets the TextEncoding
/// </summary>
private readonly Encoding textEncoding;
/// <summary>
/// Gets or sets the quantizer for reducing the color count.
/// </summary>
private readonly IQuantizer quantizer;
/// <summary>
/// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded.
/// </summary>
private readonly bool ignoreMetadata;
/// <summary>
/// The number of bits requires to store the image palette.
/// </summary>
private int bitDepth;
/// <summary>
/// Whether the current image has multiple frames.
/// </summary>
private bool hasFrames;
/// <summary>
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
/// </summary>
/// <param name="memoryManager">The <see cref="MemoryManager"/> to use for buffer allocations.</param>
/// <param name="options">The options for the encoder.</param>
public GifEncoderCore(MemoryManager memoryManager, IGifEncoderOptions options)
{
this.memoryManager = memoryManager;
this.textEncoding = options.TextEncoding ?? GifConstants.DefaultEncoding;
this.quantizer = options.Quantizer;
this.ignoreMetadata = options.IgnoreMetadata;
}
/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
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<TPixel> quantized = this.quantizer.CreateFrameQuantizer<TPixel>().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<TPixel> frame in image.Frames)
{
if (quantized == null)
{
quantized = this.quantizer.CreateFrameQuantizer<TPixel>().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);
}
/// <summary>
/// Returns the index of the most transparent color in the palette.
/// </summary>
/// <param name="quantized">
/// The quantized.
/// </param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>
/// The <see cref="int"/>.
/// </returns>
private int GetTransparentIndex<TPixel>(QuantizedFrame<TPixel> quantized)
where TPixel : struct, IPixel<TPixel>
{
// 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;
}
/// <summary>
/// Writes the file header signature and version to the stream.
/// </summary>
/// <param name="writer">The writer to write to the stream with.</param>
private void WriteHeader(EndianBinaryWriter writer)
{
writer.Write((GifConstants.FileType + GifConstants.FileVersion).ToCharArray());
}
/// <summary>
/// Writes the logical screen descriptor to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The image to encode.</param>
/// <param name="writer">The writer to write to the stream with.</param>
/// <param name="transparencyIndex">The transparency index to set the default background index to.</param>
private void WriteLogicalScreenDescriptor<TPixel>(Image<TPixel> image, EndianBinaryWriter writer, int transparencyIndex)
where TPixel : struct, IPixel<TPixel>
{
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);
}
/// <summary>
/// Writes the application extension to the stream.
/// </summary>
/// <param name="writer">The writer to write to the stream with.</param>
/// <param name="repeatCount">The animated image repeat count.</param>
/// <param name="frames">The number of image frames.</param>
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
}
}
/// <summary>
/// Writes the image comments to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to be encoded.</param>
/// <param name="writer">The stream to write to.</param>
private void WriteComments<TPixel>(Image<TPixel> image, EndianBinaryWriter writer)
where TPixel : struct, IPixel<TPixel>
{
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);
}
/// <summary>
/// Writes the graphics control extension to the stream.
/// </summary>
/// <param name="metaData">The metadata of the image or frame.</param>
/// <param name="writer">The stream to write to.</param>
/// <param name="transparencyIndex">The index of the color in the color palette to make transparent.</param>
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);
}
/// <summary>
/// Writes the image descriptor to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to be encoded.</param>
/// <param name="writer">The stream to write to.</param>
private void WriteImageDescriptor<TPixel>(ImageFrame<TPixel> image, EndianBinaryWriter writer)
where TPixel : struct, IPixel<TPixel>
{
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);
}
/// <summary>
/// Writes the color table to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode.</param>
/// <param name="writer">The writer to write to the stream with.</param>
private void WriteColorTable<TPixel>(QuantizedFrame<TPixel> image, EndianBinaryWriter writer)
where TPixel : struct, IPixel<TPixel>
{
// 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<byte> 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);
}
}
/// <summary>
/// Writes the image pixel data to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="QuantizedFrame{TPixel}"/> containing indexed pixels.</param>
/// <param name="writer">The stream to write to.</param>
private void WriteImageData<TPixel>(QuantizedFrame<TPixel> image, EndianBinaryWriter writer)
where TPixel : struct, IPixel<TPixel>
{
using (var encoder = new LzwEncoder(this.memoryManager, image.Pixels, (byte)this.bitDepth))
{
encoder.Encode(writer.BaseStream);
}
}
}
}