1
0
mirror of https://github.com/fadden/6502bench.git synced 2024-06-11 02:29:53 +00:00

Add animated GIF generation to HTML export

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.
This commit is contained in:
Andy McFadden 2019-12-24 17:53:04 -08:00
parent 226ba7c9a3
commit 6913558f4a
9 changed files with 734 additions and 21 deletions

View File

@ -59,5 +59,42 @@ namespace CommonUtil {
throw new Exception("GetWord(): should not be here");
}
/// <summary>
/// Fetches a two-byte little-endian value from a byte stream, advancing the offset.
/// </summary>
/// <param name="data">Byte stream.</param>
/// <param name="offset">Initial offset. Value will be incremented by 2.</param>
/// <returns>Value fetched.</returns>
public static ushort FetchLittleUshort(byte[] data, ref int offset) {
ushort val = (ushort)(data[offset] | (data[offset + 1] << 8));
offset += 2;
return val;
}
/// <summary>
/// Compare parts of two arrays for equality.
/// </summary>
/// <remarks>
/// We can do this faster with unsafe code. For byte arrays, see
/// https://stackoverflow.com/q/43289/294248 .
/// </remarks>
/// <param name="ar1">First array.</param>
/// <param name="offset1">Initial offset in first array.</param>
/// <param name="ar2">Second array.</param>
/// <param name="offset2">Initial offset in second array.</param>
/// <param name="count">Number of elements to compare.</param>
/// <returns>True if the array elements are equal.</returns>
public static bool CompareArrays<T>(T[] ar1, int offset1, T[] ar2, int offset2,
int count) {
Debug.Assert(count > 0);
for (int i = 0; i < count; i++) {
if (!ar1[offset1 + i].Equals(ar2[offset2 + i])) {
return false;
}
}
return true;
}
}
}

348
CommonUtil/UnpackedGif.cs Normal file
View File

@ -0,0 +1,348 @@
/*
* 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);
}
}
}
}

View File

@ -0,0 +1,235 @@
/*
* 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) {
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 size of the largest image. This will become the logical
// size of the animated GIF.
//
// TODO: 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.)
//
int maxWidth = -1;
int maxHeight = -1;
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));
}
}
}

View File

@ -48,6 +48,7 @@
<Reference Include="PresentationFramework" />
</ItemGroup>
<ItemGroup>
<Compile Include="AnimatedGifEncoder.cs" />
<Compile Include="FrameAnimationControl.xaml.cs">
<DependentUpon>FrameAnimationControl.xaml</DependentUpon>
</Compile>
@ -91,5 +92,11 @@
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CommonUtil\CommonUtil.csproj">
<Project>{a2993eac-35d8-4768-8c54-152b4e14d69c}</Project>
<Name>CommonUtil</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
@ -23,6 +24,7 @@ using System.Windows.Media.Imaging;
using Asm65;
using CommonUtil;
using CommonWPF;
namespace SourceGen {
/// <summary>
@ -687,11 +689,13 @@ namespace SourceGen {
}
/// <summary>
/// Generate one or more GIF image files, and generate references to them.
/// Generate one or more GIF image files, and output references to them.
/// </summary>
/// <param name="offset">Visualization set file offset.</param>
/// <param name="sb">String builder for the HTML output.</param>
private void OutputVisualizationSet(int offset, StringBuilder sb) {
const int IMAGE_SIZE = 64;
if (!mProject.VisualizationSets.TryGetValue(offset,
out VisualizationSet visSet)) {
sb.Append("Internal error - visualization set missing");
@ -707,27 +711,73 @@ namespace SourceGen {
string imageDirFileName = Path.GetFileName(mImageDirPath);
for (int index = 0; index < visSet.Count; index++) {
Visualization vis = visSet[index];
// Encode a GIF the same size as the original bitmap.
GifBitmapEncoder encoder = new GifBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(vis.CachedImage));
// Create new or replace existing image file.
string fileName = "vis" + offset.ToString("x6") + "_" + index.ToString("d2") +
".gif";
using (FileStream stream = new FileStream(Path.Combine(mImageDirPath, fileName),
FileMode.Create)) {
encoder.Save(stream);
string pathName = Path.Combine(mImageDirPath, fileName);
Visualization vis = visSet[index];
if (vis is VisualizationAnimation) {
VisualizationAnimation visAnim = (VisualizationAnimation)vis;
int frameDelay = PluginCommon.Util.GetFromObjDict(visAnim.VisGenParams,
VisualizationAnimation.FRAME_DELAY_MSEC_PARAM, 330);
AnimatedGifEncoder encoder = new AnimatedGifEncoder();
// Gather list of frames.
for (int i = 0; i < visAnim.Count; i++) {
Visualization avis = VisualizationSet.FindVisualizationBySerial(
mProject.VisualizationSets, visAnim[i]);
if (avis != null) {
encoder.AddFrame(BitmapFrame.Create(avis.CachedImage), frameDelay);
} else {
Debug.Assert(false); // not expected
}
}
#if false
using (MemoryStream ms = new MemoryStream()) {
encoder.Save(ms);
Debug.WriteLine("TESTING");
UnpackedGif anim = UnpackedGif.Create(ms.GetBuffer());
anim.DebugDump();
}
#endif
// Create new or replace existing image file.
try {
using (FileStream stream = new FileStream(pathName, FileMode.Create)) {
encoder.Save(stream);
}
} catch (Exception ex) {
// TODO: add an error report
Debug.WriteLine("Error creating animated GIF file '" + pathName + "': " +
ex.Message);
}
} else {
// Encode a GIF the same size as the original bitmap.
GifBitmapEncoder encoder = new GifBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(vis.CachedImage));
// Create new or replace existing image file.
try {
using (FileStream stream = new FileStream(pathName, FileMode.Create)) {
encoder.Save(stream);
}
} catch (Exception ex) {
// Something went wrong with file creation. We don't have an error
// reporting mechanism, so this will just appear as a broken or stale
// image reference.
// TODO: add an error report
Debug.WriteLine("Error creating GIF file '" + pathName + "': " +
ex.Message);
}
}
// Create as thumbnail, preserving proportions. I'm assuming most images
// will be small enough that generating a separate thumbnail would be
// Output thumbnail-size IMG element, preserving proportions. I'm assuming
// images will be small enough that generating a separate thumbnail would be
// counter-productive. This seems to look best if the height is consistent
// across all visualization lines, but that can create some monsters (e.g.
// a bitmap that's 1 pixel high and 40 wide).
int dimMult = 64;
//double maxDim = Math.Max(vis.CachedImage.Width, vis.CachedImage.Height);
// a bitmap that's 1 pixel high and 40 wide), so we cap the width.
int dimMult = IMAGE_SIZE;
double maxDim = vis.CachedImage.Height;
if (vis.CachedImage.Width > vis.CachedImage.Height * 2) {
// Too proportionally wide, so use the width as the limit. Allow it to
@ -738,8 +788,8 @@ namespace SourceGen {
}
int thumbWidth = (int)Math.Round(dimMult * (vis.CachedImage.Width / maxDim));
int thumbHeight = (int)Math.Round(dimMult * (vis.CachedImage.Height / maxDim));
Debug.WriteLine(vis.CachedImage.Width + "x" + vis.CachedImage.Height + " --> " +
thumbWidth + "x" + thumbHeight + " (" + maxDim + ")");
//Debug.WriteLine(vis.CachedImage.Width + "x" + vis.CachedImage.Height + " --> " +
// thumbWidth + "x" + thumbHeight + " (" + maxDim + ")");
if (index != 0) {
sb.Append("&nbsp;");

View File

@ -68,7 +68,9 @@ opens the Visualization Set Editor window.</p>
with the selected file offset. You can create a new visualization, edit
or remove an existing entry, or rearrange them.
If you select "New Bitmap" or edit an existing bitmap entry, the
Bitmap Visualization Editor window will open.</p>
Bitmap Visualization Editor window will open.
Similarly, if you select "New Bitmap Animation" or edit an existing
bitmap animation, the Bitmap Animation Editor will open.</p>
<h4>Bitmap Visualization Editor</h4>
@ -81,7 +83,7 @@ input parameters below the preview window may change.</p>
<p>The "tag" is a unique string that will be shown in the display list.
This is not a label, and may contain any characters you want (but leading
and trailing whitespace will be trimmed). The only requirement is that
it be unique among visualization tags.</p>
it be unique across all visualizations (bitmaps, animations, etc).</p>
<p>The preview window shows the visualizer output. The generated image is
expanded to fill the window, so small images will be shown with very
large pixels.
@ -95,6 +97,30 @@ The range of allowable values is shown to the right of the entry field.
If you enter an invalid value, the parameter description will turn red.</p>
<h4>Bitmap Animation Editor</h4>
<p>Bitmap animations allow you to create a simple animation from a
collection of other visualizations. This can be useful when a program
stores animated graphics as a series of frames.</p>
<p>The "tag" is a unique string that will be shown in the display list.
The same rules apply as for bitmap visualizations.</p>
<p>The list at the top left holds all visualizations. Select items on
the left and use the "Add" button to add them to the list on the right,
which has the set that is included in the animation. You can reorder
the list with the up/down buttons. Adding the same frame multiple times
is allowed.</p>
<p>The "frame delay" field lets you specify how long each frame is shown
on screen, in milliseconds. Some animation formats may use a different
time resolution; for example, animated GIFs use units of 1/100th of a
second. The closest value will be used. Note also that some viewers
(notably web browsers) will cap the update rate.</p>
<p>When you have one or more frames in the animation list, you can preview
the result in the window at the bottom. The actual appearance may be
slightly different, especially if the frames are different sizes. For
example, the preview window scales individual frames, but animated GIFs
will be scaled to the size of the largest frame.</p>
<h2><a name="runtime">Scripts Included with SourceGen</a></h2>
<p>A number of visualization generation scripts are included with

View File

@ -219,6 +219,7 @@ namespace SourceGen.WpfGui {
NewAnim = new VisualizationAnimation(TagString, VisualizationAnimation.ANIM_VIS_GEN,
new ReadOnlyDictionary<string, object>(visGenParams), serials, mOrigAnim);
NewAnim.GenerateImage(mEditedList);
DialogResult = true;
}
@ -290,8 +291,13 @@ namespace SourceGen.WpfGui {
}
/// <summary>
/// Adds an item to the animation list.
/// Adds an item to the end of the animation list.
/// </summary>
/// <remarks>
/// We could make this an insert or add-at-cursor operation. This feels a bit more
/// natural, and works since we're still limited to single-select on the anim list.
/// The selection should be set to the last item added so we can add repeatedly.
/// </remarks>
private void AddSelection() {
if (!IsAddEnabled) {
return;

View File

@ -260,6 +260,7 @@ namespace SourceGen.WpfGui {
if (newAnim.Count == 0) {
VisualizationList.Remove(visAnim);
} else {
newAnim.GenerateImage(CreateEditedSetList());
index = VisualizationList.IndexOf(visAnim);
VisualizationList.Remove(visAnim);
VisualizationList.Insert(index, newAnim);

View File

@ -22,6 +22,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGen", "SourceGen\Sour
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommonWPF", "CommonWPF\CommonWPF.csproj", "{1299AA2E-606D-4F3E-B3A9-3F9421E44667}"
ProjectSection(ProjectDependencies) = postProject
{A2993EAC-35D8-4768-8C54-152B4E14D69C} = {A2993EAC-35D8-4768-8C54-152B4E14D69C}
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MakeDist", "MakeDist\MakeDist.csproj", "{D9A0BD5E-8CA8-4207-81C8-DDF4BE6AB02A}"
EndProject