mirror of
https://github.com/fadden/6502bench.git
synced 2024-08-20 11:28:58 +00:00
Visualization animations are now exported as animated GIFs. The Windows stuff is a bit lame so I threw together some code that stitches a bunch of GIFs together. The GIF doesn't quite match the preview, because the preview scales the individual frames, while the animated GIF uses the largest frame as the size and is then scaled based on that. Animating frames of differing sizes together is bound to be trouble anyway, so I'm not sure how much to fret over this.
349 lines
14 KiB
C#
349 lines
14 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.Text;
|
|
|
|
namespace CommonUtil {
|
|
/// <summary>
|
|
/// Representation of a GIF image, with the various pieces broken out.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This has only been tested with the GIF images created by the Windows Media GifEncoder,
|
|
/// which are GIF89a with no global color table.
|
|
/// </remarks>
|
|
public class UnpackedGif {
|
|
//
|
|
// Header.
|
|
//
|
|
public enum FileVersion { Unknown = 0, Gif87a, Gif89a };
|
|
private static readonly byte[] GIF87A = new byte[] {
|
|
(byte)'G', (byte)'I', (byte)'F', (byte)'8', (byte)'7', (byte)'a'
|
|
};
|
|
private static readonly byte[] GIF89A = new byte[] {
|
|
(byte)'G', (byte)'I', (byte)'F', (byte)'8', (byte)'9', (byte)'a'
|
|
};
|
|
public FileVersion FileVer { get; private set; }
|
|
|
|
//
|
|
// Logical screen descriptor.
|
|
//
|
|
public ushort LogicalScreenWidth { get; private set; }
|
|
public ushort LogicalScreenHeight { get; private set; }
|
|
public bool GlobalColorTableFlag { get; private set; }
|
|
public byte ColorResolution { get; private set; }
|
|
public bool SortFlag { get; private set; }
|
|
public byte GlobalColorTableSize { get; private set; }
|
|
public byte BackgroundColorIndex { get; private set; }
|
|
public byte PixelAspectRatio { get; private set; }
|
|
|
|
//
|
|
// Global color table.
|
|
//
|
|
public byte[] GlobalColorTable { get; private set; }
|
|
|
|
//
|
|
// Extension block constants.
|
|
//
|
|
public const byte EXTENSION_INTRODUCER = 0x21;
|
|
public const byte APP_EXTENSION_LABEL = 0xff;
|
|
public const byte COMMENT_LABEL = 0xfe;
|
|
public const byte GRAPHIC_CONTROL_LABEL = 0xf9;
|
|
public const byte PLAIN_TEXT_LABEL = 0x01;
|
|
|
|
//
|
|
// Graphic control extension.
|
|
//
|
|
// These are optional. At most one may precede a graphic rendering block.
|
|
//
|
|
public class GraphicControlExtension {
|
|
public enum DisposalMethods : byte {
|
|
None = 0,
|
|
DoNotDispose = 1,
|
|
RestoreBackground = 2,
|
|
RestorePrevious = 3
|
|
}
|
|
|
|
public byte DisposalMethod { get; private set; }
|
|
public bool UserInputFlag { get; private set; }
|
|
public bool TransparencyFlag { get; private set; }
|
|
public ushort DelayTime { get; private set; }
|
|
public byte TransparentColorIndex { get; private set; }
|
|
|
|
private GraphicControlExtension() { }
|
|
|
|
public static GraphicControlExtension Create(byte[] data, ref int offset) {
|
|
Debug.Assert(data[offset] == EXTENSION_INTRODUCER &&
|
|
data[offset + 1] == GRAPHIC_CONTROL_LABEL);
|
|
|
|
GraphicControlExtension gce = new GraphicControlExtension();
|
|
|
|
offset += 2;
|
|
if (data[offset++] != 4) {
|
|
Debug.WriteLine("Bad block size in GCE data");
|
|
return null;
|
|
}
|
|
byte pak = data[offset++];
|
|
gce.DisposalMethod = (byte)((pak >> 2) & 0x07);
|
|
gce.UserInputFlag = (pak & 0x02) != 0;
|
|
gce.TransparencyFlag = (pak & 0x01) != 0;
|
|
gce.DelayTime = RawData.FetchLittleUshort(data, ref offset);
|
|
gce.TransparentColorIndex = data[offset++];
|
|
if (data[offset++] != 0) {
|
|
Debug.WriteLine("Missing termination in GCE data");
|
|
return null;
|
|
}
|
|
return gce;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Image descriptor.
|
|
//
|
|
public const byte IMAGE_SEPARATOR = 0x2c;
|
|
|
|
/// <summary>
|
|
/// The graphic rendering block is an image descriptor, followed by an optional
|
|
/// local color table, and then the image data itself.
|
|
/// </summary>
|
|
public class GraphicRenderingBlock {
|
|
public ushort ImageLeftPosition { get; private set; }
|
|
public ushort ImageTopPosition { get; private set; }
|
|
public ushort ImageWidth { get; private set; }
|
|
public ushort ImageHeight { get; private set; }
|
|
public bool LocalColorTableFlag { get; private set; }
|
|
public bool InterlaceFlag { get; private set; }
|
|
public bool SortFlag { get; private set; }
|
|
public byte LocalColorTableSize { get; private set; }
|
|
|
|
//
|
|
// Local color table.
|
|
//
|
|
public byte[] LocalColorTable { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Offset of first byte of image data (which will be the LZW minimum code size byte).
|
|
/// </summary>
|
|
public int ImageStartOffset { get; private set; }
|
|
|
|
public byte[] ImageData { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Offset of last byte of image data (which will be the terminating $00 byte).
|
|
/// </summary>
|
|
public int ImageEndOffset { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Optional extension with transparency and delay values.
|
|
/// </summary>
|
|
public GraphicControlExtension GraphicControlExt { get; set; }
|
|
|
|
private GraphicRenderingBlock() { }
|
|
|
|
public static GraphicRenderingBlock Create(byte[] data, ref int offset) {
|
|
GraphicRenderingBlock grb = new GraphicRenderingBlock();
|
|
|
|
//
|
|
// Image descriptor.
|
|
//
|
|
Debug.Assert(data[offset] == IMAGE_SEPARATOR);
|
|
offset++;
|
|
grb.ImageLeftPosition = RawData.FetchLittleUshort(data, ref offset);
|
|
grb.ImageTopPosition = RawData.FetchLittleUshort(data, ref offset);
|
|
grb.ImageWidth = RawData.FetchLittleUshort(data, ref offset);
|
|
grb.ImageHeight = RawData.FetchLittleUshort(data, ref offset);
|
|
byte pak = data[offset++];
|
|
grb.LocalColorTableFlag = (pak & 0x80) != 0;
|
|
grb.InterlaceFlag = (pak & 0x40) != 0;
|
|
grb.SortFlag = (pak & 0x20) != 0;
|
|
grb.LocalColorTableSize = (byte)(pak & 0x07);
|
|
|
|
//
|
|
// Local color table (optional).
|
|
//
|
|
if (grb.LocalColorTableFlag) {
|
|
// Size is expressed as a power of 2. TableSize=7 is 256 entries, 3 bytes each.
|
|
int tableLen = 1 << (grb.LocalColorTableSize + 1);
|
|
grb.LocalColorTable = new byte[tableLen * 3];
|
|
for (int i = 0; i < tableLen * 3; i++) {
|
|
grb.LocalColorTable[i] = data[offset++];
|
|
}
|
|
} else {
|
|
grb.LocalColorTable = new byte[0];
|
|
}
|
|
|
|
//
|
|
// Table based image data.
|
|
//
|
|
grb.ImageData = data;
|
|
grb.ImageStartOffset = offset++;
|
|
while (data[offset] != 0) {
|
|
offset += data[offset] + 1;
|
|
}
|
|
grb.ImageEndOffset = offset++;
|
|
|
|
return grb;
|
|
}
|
|
}
|
|
public List<GraphicRenderingBlock> ImageBlocks = new List<GraphicRenderingBlock>();
|
|
|
|
//
|
|
// EOF marker.
|
|
//
|
|
public const byte GIF_TRAILER = 0x3b;
|
|
|
|
|
|
/// <summary>
|
|
/// Constructor. Internal only; use factory method.
|
|
/// </summary>
|
|
private UnpackedGif() { }
|
|
|
|
/// <summary>
|
|
/// </summary>
|
|
/// <param name="gifData">GIF data stream. The array may be longer than the
|
|
/// data stream.</param>
|
|
/// <returns>Newly-created object, or null on error.</returns>
|
|
public static UnpackedGif Create(byte[] gifData) {
|
|
UnpackedGif gif = new UnpackedGif();
|
|
|
|
try {
|
|
if (!gif.Unpack(gifData)) {
|
|
return null;
|
|
}
|
|
} catch (Exception ex) {
|
|
Debug.WriteLine("Failure during GIF unpacking: " + ex);
|
|
return null;
|
|
}
|
|
|
|
return gif;
|
|
}
|
|
|
|
private bool Unpack(byte[] gifData) {
|
|
//
|
|
// Header. Signature ("GIF") + version ("87a" or "89a").
|
|
//
|
|
if (RawData.CompareArrays(gifData, 0, GIF87A, 0, GIF87A.Length)) {
|
|
FileVer = FileVersion.Gif87a;
|
|
} else if (RawData.CompareArrays(gifData, 0, GIF89A, 0, GIF87A.Length)) {
|
|
FileVer = FileVersion.Gif89a;
|
|
} else {
|
|
Debug.WriteLine("GIF signature not found");
|
|
return false;
|
|
}
|
|
//Debug.WriteLine("GIF: found signature " + FileVer);
|
|
|
|
byte pak;
|
|
|
|
//
|
|
// Logical screen descriptor.
|
|
//
|
|
int offset = GIF87A.Length;
|
|
LogicalScreenWidth = RawData.FetchLittleUshort(gifData, ref offset);
|
|
LogicalScreenHeight = RawData.FetchLittleUshort(gifData, ref offset);
|
|
pak = gifData[offset++];
|
|
GlobalColorTableFlag = (pak & 0x80) != 0;
|
|
ColorResolution = (byte)((pak >> 4) & 0x07);
|
|
SortFlag = (pak & 0x08) != 0;
|
|
GlobalColorTableSize = (byte)(pak & 0x07);
|
|
BackgroundColorIndex = gifData[offset++];
|
|
PixelAspectRatio = gifData[offset++];
|
|
|
|
//
|
|
// Global color table.
|
|
//
|
|
if (GlobalColorTableFlag) {
|
|
// Size is expressed as a power of 2. TableSize=7 is 256 entries, 3 bytes each.
|
|
int tableLen = 1 << (GlobalColorTableSize + 1);
|
|
GlobalColorTable = new byte[tableLen * 3];
|
|
for (int i = 0; i < tableLen * 3; i++) {
|
|
GlobalColorTable[i] = gifData[offset++];
|
|
}
|
|
} else {
|
|
GlobalColorTable = new byte[0];
|
|
}
|
|
|
|
//
|
|
// Various blocks follow. Continue until EOF is reached.
|
|
//
|
|
GraphicControlExtension lastGce = null;
|
|
while (true) {
|
|
if (offset >= gifData.Length) {
|
|
Debug.WriteLine("Error: GIF unpacker ran off end of buffer");
|
|
return false;
|
|
}
|
|
if (gifData[offset] == GIF_TRAILER) {
|
|
break;
|
|
} else if (gifData[offset] == EXTENSION_INTRODUCER) {
|
|
if (gifData[offset + 1] == GRAPHIC_CONTROL_LABEL) {
|
|
lastGce = GraphicControlExtension.Create(gifData, ref offset);
|
|
} else {
|
|
Debug.WriteLine("Skipping unknown extension 0x" +
|
|
gifData[offset + 1].ToString("x2"));
|
|
offset += 2;
|
|
while (gifData[offset] != 0) {
|
|
offset += gifData[offset] + 1;
|
|
}
|
|
offset++;
|
|
}
|
|
} else if (gifData[offset] == IMAGE_SEPARATOR) {
|
|
GraphicRenderingBlock grb = GraphicRenderingBlock.Create(gifData, ref offset);
|
|
if (grb != null) {
|
|
if (lastGce != null) {
|
|
grb.GraphicControlExt = lastGce;
|
|
}
|
|
ImageBlocks.Add(grb);
|
|
}
|
|
|
|
// this resets after the image
|
|
lastGce = null;
|
|
} else {
|
|
Debug.WriteLine("Found unknown block start 0x" +
|
|
gifData[offset].ToString("x2"));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public void DebugDump() {
|
|
Debug.WriteLine("UnpackedGif: " + FileVer);
|
|
Debug.WriteLine(" Logical size: " + LogicalScreenWidth + "x" + LogicalScreenHeight);
|
|
Debug.WriteLine(" Global CT: " + GlobalColorTableFlag +
|
|
" size=" + GlobalColorTableSize + " bkci=" + BackgroundColorIndex +
|
|
" sort=" + SortFlag);
|
|
Debug.WriteLine(" Aspect=" + PixelAspectRatio);
|
|
Debug.WriteLine(" Images (" + ImageBlocks.Count + "):");
|
|
foreach (GraphicRenderingBlock grb in ImageBlocks) {
|
|
if (grb.GraphicControlExt != null) {
|
|
GraphicControlExtension gce = grb.GraphicControlExt;
|
|
Debug.WriteLine(" GCE: trans=" + gce.TransparencyFlag +
|
|
" color=" + gce.TransparentColorIndex + " delay=" + gce.DelayTime +
|
|
" disp=" + gce.DisposalMethod);
|
|
} else {
|
|
Debug.WriteLine(" No GCE");
|
|
}
|
|
Debug.WriteLine(" left=" + grb.ImageLeftPosition +
|
|
" top=" + grb.ImageTopPosition + " width=" + grb.ImageWidth +
|
|
" height=" + grb.ImageHeight);
|
|
Debug.WriteLine(" localCT=" + grb.LocalColorTableFlag + " size=" +
|
|
grb.LocalColorTableSize + " itrl=" + grb.InterlaceFlag);
|
|
}
|
|
}
|
|
}
|
|
}
|