6502bench/CommonWPF/AnimatedGifEncoder.cs

238 lines
9.5 KiB
C#

/*
* Copyright 2019 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Windows.Media.Imaging;
using CommonUtil;
namespace CommonWPF {
/// <summary>
/// Creates an animated GIF from a collection of bitmap frames.
/// </summary>
public class AnimatedGifEncoder {
// GIF signature + version.
private static readonly byte[] GIF89A_SIGNATURE = new byte[] {
(byte)'G', (byte)'I', (byte)'F', (byte)'8', (byte)'9', (byte)'a'
};
private static readonly byte[] NetscapeExtStart = new byte[] {
UnpackedGif.EXTENSION_INTRODUCER,
UnpackedGif.APP_EXTENSION_LABEL,
0x0b, // Block Size
(byte)'N', // Application Identifier (8 bytes)
(byte)'E',
(byte)'T',
(byte)'S',
(byte)'C',
(byte)'A',
(byte)'P',
(byte)'E',
(byte)'2', // Appl. Authentication Code (3 bytes)
(byte)'.',
(byte)'0',
0x03, // size of block
// followed by loop flag, 2-byte repetition count, and $00 to terminate
};
private static readonly byte[] GraphicControlStart = new byte[] {
UnpackedGif.EXTENSION_INTRODUCER,
UnpackedGif.GRAPHIC_CONTROL_LABEL,
0x04, // Block Size
// followed by flags, 2-byte delay, transparency color index, and $00 to terminate
};
/// <summary>
/// List of bitmap frames.
/// </summary>
private List<BitmapFrame> Frames { get; set; }
private class MetaData {
public int DelayMsec { get; private set; }
public MetaData(int delayMsec) {
DelayMsec = delayMsec;
}
}
/// <summary>
/// Per-frame metadata.
/// </summary>
private List<MetaData> FrameData { get; set; }
/// <summary>
/// Constructor.
/// </summary>
public AnimatedGifEncoder() {
Frames = new List<BitmapFrame>();
FrameData = new List<MetaData>();
}
public void AddFrame(BitmapFrame frame, int delayMsec) {
Frames.Add(frame);
FrameData.Add(new MetaData(delayMsec));
}
/// <summary>
/// Converts the list of frames into an animated GIF, and writes it to the stream.
/// </summary>
/// <param name="stream">Output stream.</param>
public void Save(Stream stream, out int maxWidth, out int maxHeight) {
maxWidth = maxHeight = -1;
if (Frames.Count == 0) {
// nothing to do
Debug.Assert(false);
return;
}
//
// Step 1: convert all BitmapFrame objects to GIF. This lets the .NET GIF encoder
// deal with the data compression.
//
List<UnpackedGif> gifs = new List<UnpackedGif>(Frames.Count);
foreach (BitmapFrame bf in Frames) {
GifBitmapEncoder encoder = new GifBitmapEncoder();
encoder.Frames.Add(bf);
using (MemoryStream ms = new MemoryStream()) {
encoder.Save(ms);
// We're using GetBuffer() rather than ToArray() to avoid a copy. One
// consequence of this choice is that the byte[] may be oversized. Since
// GIFs are treated as streams with explicit termination this should not
// pose a problem.
gifs.Add(UnpackedGif.Create(ms.GetBuffer()));
}
}
//
// Step 2: determine the dimensions of the largest image. This will become the
// logical size of the animated GIF.
//
// TODO(maybe): We have an opportunity to replace all of the local color tables with a
// single global color table. This is only possible if all of the local tables are
// identical and the transparency values in the GCE also match up. (Well, it's
// otherwise *possible*, but we'd need to decode, update palettes and pixels, and
// re-encode.)
// TODO(maybe): add an arg to Save() that causes it to use the first bitmap's
// palette as the global palette.
//
foreach (UnpackedGif gif in gifs) {
//gif.DebugDump();
if (maxWidth < gif.LogicalScreenWidth) {
maxWidth = gif.LogicalScreenWidth;
}
if (maxHeight < gif.LogicalScreenHeight) {
maxHeight = gif.LogicalScreenHeight;
}
}
if (maxWidth < 0 || maxHeight < 0) {
Debug.WriteLine("Unable to determine correct width/height");
return;
}
//
// Step 3: output data.
//
stream.Write(GIF89A_SIGNATURE, 0, GIF89A_SIGNATURE.Length);
WriteLittleUshort(stream, (ushort)maxWidth);
WriteLittleUshort(stream, (ushort)maxHeight);
stream.WriteByte(0x70); // no GCT; max color resolution (does this matter?)
stream.WriteByte(0); // BCI; not relevant
stream.WriteByte(0); // no aspect ratio adjustment
stream.Write(NetscapeExtStart, 0, NetscapeExtStart.Length);
stream.WriteByte(1); // yes, we want to loop
WriteLittleUshort(stream, 0); // loop forever
stream.WriteByte(0); // end of block
Debug.Assert(gifs.Count == FrameData.Count);
for (int i = 0; i < Frames.Count; i++) {
UnpackedGif gif = gifs[i];
MetaData md = FrameData[i];
// Just use the first image.
UnpackedGif.GraphicRenderingBlock grb = gif.ImageBlocks[0];
byte colorTableSize;
byte[] colorTable;
if (grb.LocalColorTableFlag) {
colorTableSize = grb.LocalColorTableSize;
colorTable = grb.LocalColorTable;
} else if (gif.GlobalColorTableFlag) {
colorTableSize = gif.GlobalColorTableSize;
colorTable = gif.GlobalColorTable;
} else {
Debug.Assert(false);
colorTableSize = 0x07;
colorTable = new byte[256 * 3]; // a whole lotta black
}
Debug.Assert(colorTable.Length == (1 << (colorTableSize + 1)) * 3);
// If it has a GCE, use that. Otherwise supply default values. Either way
// we use the frame delay from the meta-data.
UnpackedGif.GraphicControlExtension gce = grb.GraphicControlExt;
byte disposalMethod =
(byte)UnpackedGif.GraphicControlExtension.DisposalMethods.RestoreBackground;
bool userInputFlag = false;
bool transparencyFlag = false;
byte transparentColorIndex = 0;
if (gce != null) {
//disposalMethod = gce.DisposalMethod;
userInputFlag = gce.UserInputFlag;
transparencyFlag = gce.TransparencyFlag;
transparentColorIndex = gce.TransparentColorIndex;
}
stream.Write(GraphicControlStart, 0, GraphicControlStart.Length);
stream.WriteByte((byte)((disposalMethod << 2) |
(userInputFlag ? 0x02 : 0) | (transparencyFlag ? 0x01 : 0)));
WriteLittleUshort(stream, (ushort)Math.Round(md.DelayMsec / 10.0));
stream.WriteByte(transparentColorIndex);
stream.WriteByte(0); // end of GCE
// Output image descriptor. We can center the images in the animation or
// just leave them in the top-left corner.
stream.WriteByte(UnpackedGif.IMAGE_SEPARATOR);
WriteLittleUshort(stream, 0); // left
WriteLittleUshort(stream, 0); // top
WriteLittleUshort(stream, gif.LogicalScreenWidth);
WriteLittleUshort(stream, gif.LogicalScreenHeight);
stream.WriteByte((byte)(0x80 | colorTableSize)); // local table, no sort/intrl
// Local color table.
stream.Write(colorTable, 0, colorTable.Length);
// Image data. Trailing $00 is included.
stream.Write(grb.ImageData, grb.ImageStartOffset,
grb.ImageEndOffset - grb.ImageStartOffset + 1);
}
stream.WriteByte(UnpackedGif.GIF_TRAILER);
}
private static void WriteLittleUshort(Stream stream, ushort val) {
stream.WriteByte((byte)val);
stream.WriteByte((byte)(val >> 8));
}
}
}