diff --git a/SourceGen/AppSettings.cs b/SourceGen/AppSettings.cs index d747182..62abcd5 100644 --- a/SourceGen/AppSettings.cs +++ b/SourceGen/AppSettings.cs @@ -131,6 +131,7 @@ namespace SourceGen { public const string EXPORT_TEXT_MODE = "export-text-mode"; public const string EXPORT_SELECTION_ONLY = "export-selection-only"; public const string EXPORT_LONG_LABEL_NEW_LINE = "export-long-label-new-line"; + public const string EXPORT_GENERATE_IMAGE_FILES = "export-generate-image-files"; // Internal debugging features. public const string DEBUG_MENU_ENABLED = "debug-menu-enabled"; diff --git a/SourceGen/Exporter.cs b/SourceGen/Exporter.cs index 0e2fa1c..b12ff19 100644 --- a/SourceGen/Exporter.cs +++ b/SourceGen/Exporter.cs @@ -19,6 +19,7 @@ using System.IO; using System.Text; using System.Windows; using System.Windows.Media; +using System.Windows.Media.Imaging; using Asm65; using CommonUtil; @@ -43,6 +44,11 @@ namespace SourceGen { /// public bool IncludeNotes { get; set; } + /// + /// Should image files be generated? + /// + public bool GenerateImageFiles { get; set; } + /// /// If set, labels that are wider than the label column should go on their own line. /// @@ -83,6 +89,11 @@ namespace SourceGen { /// private Formatter mFormatter; + /// + /// Directory path for image files. + /// + private string mImageDirPath; + /// /// The cumulative width of the columns determines the start point. /// @@ -315,7 +326,7 @@ namespace SourceGen { TextUtil.AppendPaddedString(sb, parts.Comment, mColStart[(int)Col.Label]); break; case LineListGen.Line.Type.VisualizationSet: - break; // TODO(xyzzy) + return; // show nothing case LineListGen.Line.Type.Blank: break; default: @@ -371,8 +382,8 @@ namespace SourceGen { /// /// Generates HTML output to the specified path. /// - /// Full pathname of output file. This defines the root - /// directory if there are additional files. + /// Full pathname of output file (including ".html"). This + /// defines the root directory if there are additional files. /// If set, existing CSS file will be replaced. public void OutputToHtml(string pathName, bool overwriteCss) { string exportTemplate = RuntimeDataAccess.GetPathName(HTML_EXPORT_TEMPLATE); @@ -388,6 +399,41 @@ namespace SourceGen { return; } + // We should only need the _IMG directory if there are visualizations. + if (GenerateImageFiles && mProject.VisualizationSets.Count != 0) { + string imageDirName = Path.GetFileNameWithoutExtension(pathName) + "_IMG"; + string imageDirPath = Path.Combine(Path.GetDirectoryName(pathName), imageDirName); + bool exists = false; + try { + FileAttributes attr = File.GetAttributes(imageDirPath); + if ((attr & FileAttributes.Directory) != FileAttributes.Directory) { + string msg = string.Format(Res.Strings.ERR_FILE_EXISTS_NOT_DIR_FMT, + imageDirPath); + MessageBox.Show(msg, Res.Strings.ERR_FILE_GENERIC_CAPTION, + MessageBoxButton.OK, MessageBoxImage.Error); + return; + } + exists = true; + } catch (FileNotFoundException) { + } catch (DirectoryNotFoundException) { + } + + if (!exists) { + try { + Directory.CreateDirectory(imageDirPath); + } catch (Exception ex) { + string msg = string.Format(Res.Strings.ERR_DIR_CREATE_FAILED_FMT, + imageDirPath, ex.Message); + MessageBox.Show(msg, Res.Strings.ERR_FILE_GENERIC_CAPTION, + MessageBoxButton.OK, MessageBoxImage.Error); + return; + } + } + + // All good. + mImageDirPath = imageDirPath; + } + // Perform some quick substitutions. This could be done more efficiently, // but we're only doing this on the template file, which should be small. tmplStr = tmplStr.Replace("$ProjectName$", mProject.DataFileName); @@ -620,7 +666,16 @@ namespace SourceGen { parts.Comment.Length); break; case LineListGen.Line.Type.VisualizationSet: - break; // TODO(xyzzy) + if (!GenerateImageFiles) { + // generate nothing at all + return; + } + while (colPos < mColStart[(int)Col.Label]) { + sb.Append(' '); + colPos++; + } + OutputVisualizationSet(line.FileOffset, sb); + break; case LineListGen.Line.Type.Blank: break; default: @@ -631,6 +686,73 @@ namespace SourceGen { tw.WriteLine(sb); } + /// + /// Generate one or more GIF image files, and generate references to them. + /// + /// Visualization set file offset. + /// String builder for the HTML output. + private void OutputVisualizationSet(int offset, StringBuilder sb) { + if (!mProject.VisualizationSets.TryGetValue(offset, + out VisualizationSet visSet)) { + sb.Append("Internal error - visualization set missing"); + Debug.Assert(false); + return; + } + if (visSet.Count == 0) { + sb.Append("Internal error - empty visualization set"); + Debug.Assert(false); + return; + } + + 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); + } + + // Create as thumbnail, preserving proportions. I'm assuming most 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); + 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 + // up to 2x the max width (which can't cause the thumb height to exceed + // the height limit). + maxDim = vis.CachedImage.Width; + dimMult *= 2; + } + 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 + ")"); + + if (index != 0) { + sb.Append(" "); + } + + sb.Append("\"vis\""); + } + } + /// /// Appends a string to the string buffer. If the number of characters in the buffer /// is less than the desired start position, spaces will be added. At least one space diff --git a/SourceGen/LineListGen.cs b/SourceGen/LineListGen.cs index 0bb9088..2138fe2 100644 --- a/SourceGen/LineListGen.cs +++ b/SourceGen/LineListGen.cs @@ -957,6 +957,12 @@ namespace SourceGen { mProject.GetAnattrib(offset - 1).IsData) { // Transition from data to code. (Don't add blank line for inline data.) lines.Add(GenerateBlankLine(offset)); + } else if (mProject.VisualizationSets.ContainsKey(offset) && !addBlank && + offset != 0) { + // Blank line before visualization set helps keep image visually grouped + // with its data. (Slightly weird things happen with .ORG at the start of + // the file; don't try to add a blank there.) + lines.Add(GenerateBlankLine(offset)); } else if (addBlank) { // Previous instruction wanted to be followed by a blank line. lines.Add(GenerateBlankLine(offset)); diff --git a/SourceGen/MainController.cs b/SourceGen/MainController.cs index dac50bb..fbc64c6 100644 --- a/SourceGen/MainController.cs +++ b/SourceGen/MainController.cs @@ -2205,6 +2205,7 @@ namespace SourceGen { Exporter eport = new Exporter(mProject, CodeLineList, mOutputFormatter, dlg.ColFlags, rightWidths); eport.IncludeNotes = dlg.IncludeNotes; + eport.GenerateImageFiles = dlg.GenerateImageFiles; eport.LongLabelNewLine = dlg.LongLabelNewLine; if (dlg.SelectionOnly) { DisplayListSelection selection = mMainWin.CodeDisplayList.SelectedIndices; diff --git a/SourceGen/Res/Strings.xaml b/SourceGen/Res/Strings.xaml index 886ca7a..df8de73 100644 --- a/SourceGen/Res/Strings.xaml +++ b/SourceGen/Res/Strings.xaml @@ -56,6 +56,7 @@ limitations under the License. Type hint not recognized Invalid visualization item: {0} Invalid visualization set at +{0:x6} + Unable to create directory {0}: {1} Removed duplicate label '{0}' (offset +{1:x6}) Failed copying {0} to {1}: {2}. The file {0} exists, but is not a directory. diff --git a/SourceGen/Res/Strings.xaml.cs b/SourceGen/Res/Strings.xaml.cs index acaa626..38dd8a4 100644 --- a/SourceGen/Res/Strings.xaml.cs +++ b/SourceGen/Res/Strings.xaml.cs @@ -93,6 +93,8 @@ namespace SourceGen.Res { (string)Application.Current.FindResource("str_ErrBadVisualizationFmt"); public static string ERR_BAD_VISUALIZATION_SET_FMT = (string)Application.Current.FindResource("str_ErrBadVisualizationSetFmt"); + public static string ERR_DIR_CREATE_FAILED_FMT = + (string)Application.Current.FindResource("str_ErrDirCreateFailedFmt"); public static string ERR_DUPLICATE_LABEL_FMT = (string)Application.Current.FindResource("str_ErrDuplicateLabelFmt"); public static string ERR_FILE_COPY_FAILED_FMT = diff --git a/SourceGen/RuntimeData/Help/codegen.html b/SourceGen/RuntimeData/Help/codegen.html index 6f3949e..7b75b53 100644 --- a/SourceGen/RuntimeData/Help/codegen.html +++ b/SourceGen/RuntimeData/Help/codegen.html @@ -323,7 +323,7 @@ assembler.

The text output is similar to what you'd get by copying lines to the clipboard and pasting them into a text file, except that you have greater control over which columns are included. The HTML version is augmented -with links.

+with links and (optionally) images.

Use File > Export to open the export dialog. You have several options:

@@ -346,6 +346,8 @@ options:

plain text and Comma-Separated Value format. The latter is useful for importing source code into another application, such as a spreadsheet. +
  • Generate image files. When exporting to HTML, selecting this + will cause GIF images to be generated for visualizations.
  • Overwrite CSS file. Some aspects of the HTML output's format are defined by a file called "SGStyle.css", which may be shared between multiple HTML files and customized. The file is copied out diff --git a/SourceGen/RuntimeData/SGStyle.css b/SourceGen/RuntimeData/SGStyle.css index e75b426..3be2f60 100644 --- a/SourceGen/RuntimeData/SGStyle.css +++ b/SourceGen/RuntimeData/SGStyle.css @@ -23,4 +23,20 @@ a[href^="#"]:link, a[href^="#"]:visited { a[href^="#"]:hover { color: blue; text-decoration: underline; -} \ No newline at end of file +} + +/* + * Put a border around visualizations. Disable smoothing so they don't + * blur out. + * + * Pixelated rendering works in Chrome, Safari, and IE11, but doesn't in + * current versions of Edge and Firefox. We could do it with Javascript + * Canvas (e.g. https://stackoverflow.com/a/19129822/294248), or by + * generating scaled thumbnail files directly. + */ +.vis { + border: 1px solid gray; + + -ms-interpolation-mode: nearest-neighbor; + image-rendering: pixelated; +} diff --git a/SourceGen/SGTestData/Visualization/apple2-bitmap-test#061000.dis65 b/SourceGen/SGTestData/Visualization/apple2-bitmap-test#061000.dis65 index a064771..2db7f7d 100644 --- a/SourceGen/SGTestData/Visualization/apple2-bitmap-test#061000.dis65 +++ b/SourceGen/SGTestData/Visualization/apple2-bitmap-test#061000.dis65 @@ -19,7 +19,10 @@ "Comments":{ }, "LongComments":{ -}, +"222":{ +"Text":"\r\nPurple/green diagonal\r\n\r\n","BoxMode":false,"MaxWidth":80,"BackgroundColor":0}, +"0":{ +"Text":"Some images:","BoxMode":false,"MaxWidth":80,"BackgroundColor":0}}, "Notes":{ "222":{ "Text":"Bitmaps here","BoxMode":false,"MaxWidth":80,"BackgroundColor":-256}}, @@ -43,6 +46,19 @@ "LvTables":{ }, "VisualizationSets":{ +"0":{ +"Items":[{ +"Tag":"multi1","VisGenIdent":"apple2-hi-res-bitmap","VisGenParams":{ +"offset":222,"byteWidth":1,"height":8,"colStride":0,"rowStride":0,"isColor":true,"isFirstOdd":false}}, +{ +"Tag":"multi2","VisGenIdent":"apple2-hi-res-bitmap","VisGenParams":{ +"offset":230,"byteWidth":1,"height":8,"colStride":0,"rowStride":0,"isColor":true,"isFirstOdd":false}}, +{ +"Tag":"multi3","VisGenIdent":"apple2-hi-res-bitmap","VisGenParams":{ +"offset":238,"byteWidth":12,"height":1,"colStride":0,"rowStride":0,"isColor":false,"isFirstOdd":false}}, +{ +"Tag":"multi4","VisGenIdent":"apple2-hi-res-bitmap","VisGenParams":{ +"offset":270,"byteWidth":3,"height":8,"colStride":0,"rowStride":0,"isColor":true,"isFirstOdd":false}}]}, "222":{ "Items":[{ "Tag":"vis0000de","VisGenIdent":"apple2-hi-res-bitmap","VisGenParams":{ diff --git a/SourceGen/Sandbox/PluginDllCache.cs b/SourceGen/Sandbox/PluginDllCache.cs index adf5f8c..e36fd40 100644 --- a/SourceGen/Sandbox/PluginDllCache.cs +++ b/SourceGen/Sandbox/PluginDllCache.cs @@ -79,6 +79,7 @@ namespace SourceGen.Sandbox { /// Prepares the plugin directory. Creates it and copies PluginCommon.dll in. /// Throws an exception if something fails. ///
  • + /// Various failures from file/dir operations. public static void PreparePluginDir() { string dstDir = GetPluginDirPath(); if (File.Exists(dstDir) && !Directory.Exists(dstDir)) { diff --git a/SourceGen/Visualization.cs b/SourceGen/Visualization.cs index 64e0fc6..3ece55d 100644 --- a/SourceGen/Visualization.cs +++ b/SourceGen/Visualization.cs @@ -47,7 +47,8 @@ namespace SourceGen { public ReadOnlyDictionary VisGenParams { get; private set; } /// - /// Cached reference to 2D image, useful for thumbnails. Not serialized. + /// Cached reference to 2D image, useful for thumbnails. Not serialized. This always + /// has an image reference; in times of trouble it will point at BROKEN_IMAGE. /// /// /// Because the underlying data never changes, we only need to regenerate the diff --git a/SourceGen/WpfGui/Export.xaml b/SourceGen/WpfGui/Export.xaml index de21f46..6b246c5 100644 --- a/SourceGen/WpfGui/Export.xaml +++ b/SourceGen/WpfGui/Export.xaml @@ -102,6 +102,7 @@ limitations under the License. + diff --git a/SourceGen/WpfGui/Export.xaml.cs b/SourceGen/WpfGui/Export.xaml.cs index 96694dd..b19eabe 100644 --- a/SourceGen/WpfGui/Export.xaml.cs +++ b/SourceGen/WpfGui/Export.xaml.cs @@ -165,6 +165,12 @@ namespace SourceGen.WpfGui { set { mOverwriteCss = value; OnPropertyChanged(); } } + private bool mGenerateImageFiles; + public bool GenerateImageFiles { + get { return mGenerateImageFiles; } + set { mGenerateImageFiles = value; OnPropertyChanged(); } + } + private enum TextMode { Unknown = 0, PlainText, @@ -210,6 +216,8 @@ namespace SourceGen.WpfGui { SelectionOnly = AppSettings.Global.GetBool(AppSettings.EXPORT_SELECTION_ONLY, false); LongLabelNewLine = AppSettings.Global.GetBool(AppSettings.EXPORT_LONG_LABEL_NEW_LINE, false); + GenerateImageFiles = + AppSettings.Global.GetBool(AppSettings.EXPORT_GENERATE_IMAGE_FILES, true); int[] colWidths = new int[] { 9, 8, 11, 72 }; // 100-col output string colStr = AppSettings.Global.GetString(AppSettings.EXPORT_COL_WIDTHS, null); @@ -248,6 +256,7 @@ namespace SourceGen.WpfGui { AppSettings.Global.SetBool(AppSettings.EXPORT_SHOW_ATTR, ShowAttr); AppSettings.Global.SetBool(AppSettings.EXPORT_SELECTION_ONLY, SelectionOnly); AppSettings.Global.SetBool(AppSettings.EXPORT_LONG_LABEL_NEW_LINE, LongLabelNewLine); + AppSettings.Global.SetBool(AppSettings.EXPORT_GENERATE_IMAGE_FILES,GenerateImageFiles); int[] colWidths = new int[] { AsmLabelColWidth, AsmOpcodeColWidth, AsmOperandColWidth, AsmCommentColWidth };