From 75ccffe393aef6cad4a2047f1548244fad0bb856 Mon Sep 17 00:00:00 2001 From: Andy McFadden Date: Wed, 12 Jan 2022 11:48:20 -0800 Subject: [PATCH] Update visualization bitmap API The existing API was better suited to direct color than indexed color. The NES visualizer was using a slightly silly hack to avoid duplicate colors; this has been removed. --- CommonUtil/UnpackedGif.cs | 4 ++ PluginCommon/Interfaces.cs | 17 +++--- PluginCommon/VisBitmap8.cs | 54 +++++++++++++++----- SourceGen/Exporter.cs | 2 +- SourceGen/RuntimeData/Nintendo/VisNES.cs | 30 +++++------ SourceGen/WpfGui/ExportVisualization.xaml.cs | 18 +++++-- 6 files changed, 85 insertions(+), 40 deletions(-) diff --git a/CommonUtil/UnpackedGif.cs b/CommonUtil/UnpackedGif.cs index a7acd18..4eadfbe 100644 --- a/CommonUtil/UnpackedGif.cs +++ b/CommonUtil/UnpackedGif.cs @@ -25,6 +25,10 @@ namespace CommonUtil { /// /// This has only been tested with the GIF images created by the Windows Media GifEncoder, /// which are GIF89a with no global color table. + /// + /// References: + /// https://www.w3.org/Graphics/GIF/spec-gif87.txt + /// https://www.w3.org/Graphics/GIF/spec-gif89a.txt /// public class UnpackedGif { // diff --git a/PluginCommon/Interfaces.cs b/PluginCommon/Interfaces.cs index d1e1c45..832cac3 100644 --- a/PluginCommon/Interfaces.cs +++ b/PluginCommon/Interfaces.cs @@ -308,20 +308,23 @@ namespace PluginCommon { /// int Height { get; } - //void SetPixelIndex(int x, int y, byte colorIndex); - //int GetPixel(int x, int y); // returns ARGB value - /// - /// Returns a densely-packed array of color indices or ARGB values. + /// Returns a densely-packed array of color indices or ARGB values. Color index + /// values may be remapped from what was originally set to improve compression. + /// /// Do not modify. /// byte[] GetPixels(); /// - /// Returns the color palette as a series of 32-bit ARGB values. Will be null for - /// direct-color images. - /// Do not modify. + /// Returns the color palette as a series of 32-bit ARGB values. Duplicate entries + /// may have been merged to improve compression. + /// + /// Will be null for direct-color images. Do not modify. /// + /// + /// It's possible, but weird, for the array to have a length of zero. + /// int[] GetPalette(); // TODO(maybe): report pixel aspect ratio? diff --git a/PluginCommon/VisBitmap8.cs b/PluginCommon/VisBitmap8.cs index 61afd4d..d9ee6d9 100644 --- a/PluginCommon/VisBitmap8.cs +++ b/PluginCommon/VisBitmap8.cs @@ -22,6 +22,9 @@ namespace PluginCommon { /// /// Bitmap with 8-bit palette indices, for use with visualization generators. /// + /// + /// The bitmap is initially filled with color index 0. + /// [Serializable] public class VisBitmap8 : IVisualization2d { public const int MAX_DIMENSION = 4096; @@ -34,7 +37,7 @@ namespace PluginCommon { private byte[] mData; private int[] mPalette; - private int mNextColorIdx; + private int mMaxColorIndex; /// @@ -51,10 +54,11 @@ namespace PluginCommon { Height = height; mData = new byte[width * height]; - mPalette = new int[256]; - mNextColorIdx = 0; + mPalette = new int[256]; // entries initialize to 0, i.e. transparent black + mMaxColorIndex = 0; } + //[Obsolete("use GetPixelIndex()")] public int GetPixel(int x, int y) { byte pix = mData[x + y * Width]; return mPalette[pix]; @@ -75,9 +79,9 @@ namespace PluginCommon { throw new ArgumentException("Bad x/y: " + x + "," + y + " (width=" + Width + " height=" + Height + ")"); } - if (colorIndex < 0 || colorIndex >= mNextColorIdx) { + if (colorIndex < 0 || colorIndex >= mMaxColorIndex) { throw new ArgumentException("Bad color: " + colorIndex + " (nextCol=" + - mNextColorIdx + ")"); + mMaxColorIndex + ")"); } mData[x + y * Width] = colorIndex; } @@ -87,9 +91,9 @@ namespace PluginCommon { /// /// Color index. public void SetAllPixelIndices(byte colorIndex) { - if (colorIndex < 0 || colorIndex >= mNextColorIdx) { + if (colorIndex < 0 || colorIndex >= mMaxColorIndex) { throw new ArgumentException("Bad color: " + colorIndex + " (nextCol=" + - mNextColorIdx + ")"); + mMaxColorIndex + ")"); } for (int i = 0; i < mData.Length; i++) { mData[i] = colorIndex; @@ -98,13 +102,15 @@ namespace PluginCommon { // IVisualization2d public byte[] GetPixels() { + // TODO: remap any duplicate colors to reduce size of GIF return mData; } // IVisualization2d public int[] GetPalette() { - int[] pal = new int[mNextColorIdx]; - for (int i = 0; i < mNextColorIdx; i++) { + // TODO: remove any duplicate colors to reduce size of GIF + int[] pal = new int[mMaxColorIndex]; + for (int i = 0; i < mMaxColorIndex; i++) { pal[i] = mPalette[i]; } return pal; @@ -116,20 +122,21 @@ namespace PluginCommon { /// effect. /// /// 32-bit ARGB color value. + //[Obsolete("use SetColor()")] public void AddColor(int color) { - if (mNextColorIdx == 256) { + if (mMaxColorIndex == 256) { Debug.WriteLine("Palette is full"); return; } // I'm expecting palettes to only have a few colors, so O(n^2) is fine for now. - for (int i = 0; i < mNextColorIdx; i++) { + for (int i = 0; i < mMaxColorIndex; i++) { if (mPalette[i] == color) { Debug.WriteLine("Color " + color.ToString("x6") + " already exists in palette (" + i + ")"); return; } } - mPalette[mNextColorIdx++] = color; + mPalette[mMaxColorIndex++] = color; } /// @@ -140,10 +147,33 @@ namespace PluginCommon { /// Red value. /// Green value. /// Blue value. + //[Obsolete("use SetColor()")] public void AddColor(byte a, byte r, byte g, byte b) { AddColor(Util.MakeARGB(a, r, g, b)); } + /// + /// Sets the Nth entry in the color palette. + /// + /// + /// The size of the color palette will expand to hold the largest index. For best + /// results, start with index 0 and count up, avoiding duplicates. + /// + /// Palette index, 0-255. + /// Alpha value. + /// Red value. + /// Green value. + /// Blue value. + public void SetColor(int index, byte a, byte r, byte g, byte b) { + if (index < 0 || index > 255) { + throw new ArgumentException("Invalid index: " + index); + } + mPalette[index] = Util.MakeARGB(a, r, g, b); + if (index >= mMaxColorIndex) { + mMaxColorIndex = index + 1; + } + } + /// /// Draws an 8x8 character cell on the bitmap. /// diff --git a/SourceGen/Exporter.cs b/SourceGen/Exporter.cs index 8dddb9f..a3922dd 100644 --- a/SourceGen/Exporter.cs +++ b/SourceGen/Exporter.cs @@ -886,7 +886,7 @@ namespace SourceGen { #if false // try feeding the animated GIF into our GIF unpacker using (MemoryStream ms = new MemoryStream()) { - encoder.Save(ms); + encoder.Save(ms, out dispWidth, out dispHeight); Debug.WriteLine("TESTING"); UnpackedGif anim = UnpackedGif.Create(ms.GetBuffer()); anim.DebugDump(); diff --git a/SourceGen/RuntimeData/Nintendo/VisNES.cs b/SourceGen/RuntimeData/Nintendo/VisNES.cs index 9ad53ae..a33cdde 100644 --- a/SourceGen/RuntimeData/Nintendo/VisNES.cs +++ b/SourceGen/RuntimeData/Nintendo/VisNES.cs @@ -276,29 +276,29 @@ namespace RuntimeData.Nintendo { } private void SetPalette(VisBitmap8 vb, Palette pal) { - vb.AddColor(0, 0, 0, 0); // 0=transparent - vb.AddColor(0xff, 0x01, 0x01, 0x01); // 1=near black (so VB doesn't uniquify) - vb.AddColor(0xff, 0xfe, 0xfe, 0xfe); // 2=near white + vb.SetColor((byte)Color.Transparent, 0x00, 0x00, 0x00, 0x00); + vb.SetColor((byte)Color.Black, 0xff, 0x00, 0x00, 0x00); + vb.SetColor((byte)Color.White, 0xff, 0xff, 0xff, 0xff); switch (pal) { case Palette.Greyscale: default: - vb.AddColor(0xff, 0x00, 0x00, 0x00); // black - vb.AddColor(0xff, 0x80, 0x80, 0x80); // dark grey - vb.AddColor(0xff, 0xb0, 0xb0, 0xb0); // medium grey - vb.AddColor(0xff, 0xe0, 0xe0, 0xe0); // light grey + vb.SetColor((byte)Color.Color0, 0xff, 0x00, 0x00, 0x00); // black + vb.SetColor((byte)Color.Color1, 0xff, 0x80, 0x80, 0x80); // dark grey + vb.SetColor((byte)Color.Color2, 0xff, 0xb0, 0xb0, 0xb0); // medium grey + vb.SetColor((byte)Color.Color3, 0xff, 0xe0, 0xe0, 0xe0); // light grey break; case Palette.Pinkish: - vb.AddColor(0xff, 0x49, 0x99, 0xfe); // sky blue - vb.AddColor(0xff, 0xff, 0xbd, 0xaf); // pinkish - vb.AddColor(0xff, 0xcd, 0x50, 0x00); // dark orange - vb.AddColor(0xff, 0x00, 0x00, 0x00); // black + vb.SetColor((byte)Color.Color0, 0xff, 0x49, 0x99, 0xfe); // sky blue + vb.SetColor((byte)Color.Color1, 0xff, 0xff, 0xbd, 0xaf); // pinkish + vb.SetColor((byte)Color.Color2, 0xff, 0xcd, 0x50, 0x00); // dark orange + vb.SetColor((byte)Color.Color3, 0xff, 0x00, 0x00, 0x00); // black break; case Palette.Greenish: - vb.AddColor(0xff, 0x49, 0x99, 0xfe); // sky blue - vb.AddColor(0xff, 0x00, 0xa4, 0x00); // medium green - vb.AddColor(0xff, 0xfc, 0xfc, 0xfc); // near white - vb.AddColor(0xff, 0xff, 0x99, 0x2b); // orange + vb.SetColor((byte)Color.Color0, 0xff, 0x49, 0x99, 0xfe); // sky blue + vb.SetColor((byte)Color.Color1, 0xff, 0x00, 0xa4, 0x00); // medium green + vb.SetColor((byte)Color.Color2, 0xff, 0xfc, 0xfc, 0xfc); // near white + vb.SetColor((byte)Color.Color3, 0xff, 0xff, 0x99, 0x2b); // orange break; } } diff --git a/SourceGen/WpfGui/ExportVisualization.xaml.cs b/SourceGen/WpfGui/ExportVisualization.xaml.cs index 307a8c4..914b7a7 100644 --- a/SourceGen/WpfGui/ExportVisualization.xaml.cs +++ b/SourceGen/WpfGui/ExportVisualization.xaml.cs @@ -15,19 +15,17 @@ */ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; using System.Windows.Media.Imaging; - -using CommonWPF; using Microsoft.Win32; +using CommonUtil; +using CommonWPF; + namespace SourceGen.WpfGui { /// /// Export an image from the visualization editor. @@ -157,9 +155,19 @@ namespace SourceGen.WpfGui { GifBitmapEncoder encoder = new GifBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(outImage)); +#if false + // try feeding the GIF into our GIF unpacker + using (MemoryStream ms = new MemoryStream()) { + encoder.Save(ms); + Debug.WriteLine("TESTING"); + UnpackedGif anim = UnpackedGif.Create(ms.GetBuffer()); + anim.DebugDump(); + } +#else using (FileStream stream = new FileStream(pathName, FileMode.Create)) { encoder.Save(stream); } +#endif } } catch (Exception ex) { // Error handling is a little sloppy, but this shouldn't fail often.