mirror of
https://github.com/fadden/6502bench.git
synced 2024-08-20 11:28:58 +00:00
238 lines
9.5 KiB
C#
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));
|
|
}
|
|
}
|
|
}
|