1
0
mirror of https://github.com/fadden/6502bench.git synced 2024-11-25 14:34:27 +00:00

Add visualization sets to exported HTML

We now generate GIF images for visualizations and add inline
references to them in the HTML output.

Images are scaled using the HTML img properties.  This works well
on some browsers, but others insist on "smooth" scaling that blurs
out the pixels.  This may require a workaround.

An extra blank line is now added above visualizations.  This helps
keep the image and data visually grouped.

The Apple II bitmap test project was updated to have a visualization
set with multiple images at the top of the file.
This commit is contained in:
Andy McFadden 2019-12-13 16:52:50 -08:00
parent 0b0944e0fc
commit 5d9b9753e8
13 changed files with 187 additions and 8 deletions

View File

@ -131,6 +131,7 @@ namespace SourceGen {
public const string EXPORT_TEXT_MODE = "export-text-mode"; public const string EXPORT_TEXT_MODE = "export-text-mode";
public const string EXPORT_SELECTION_ONLY = "export-selection-only"; 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_LONG_LABEL_NEW_LINE = "export-long-label-new-line";
public const string EXPORT_GENERATE_IMAGE_FILES = "export-generate-image-files";
// Internal debugging features. // Internal debugging features.
public const string DEBUG_MENU_ENABLED = "debug-menu-enabled"; public const string DEBUG_MENU_ENABLED = "debug-menu-enabled";

View File

@ -19,6 +19,7 @@ using System.IO;
using System.Text; using System.Text;
using System.Windows; using System.Windows;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging;
using Asm65; using Asm65;
using CommonUtil; using CommonUtil;
@ -43,6 +44,11 @@ namespace SourceGen {
/// </summary> /// </summary>
public bool IncludeNotes { get; set; } public bool IncludeNotes { get; set; }
/// <summary>
/// Should image files be generated?
/// </summary>
public bool GenerateImageFiles { get; set; }
/// <summary> /// <summary>
/// If set, labels that are wider than the label column should go on their own line. /// If set, labels that are wider than the label column should go on their own line.
/// </summary> /// </summary>
@ -83,6 +89,11 @@ namespace SourceGen {
/// </summary> /// </summary>
private Formatter mFormatter; private Formatter mFormatter;
/// <summary>
/// Directory path for image files.
/// </summary>
private string mImageDirPath;
/// <summary> /// <summary>
/// The cumulative width of the columns determines the start point. /// The cumulative width of the columns determines the start point.
/// </summary> /// </summary>
@ -315,7 +326,7 @@ namespace SourceGen {
TextUtil.AppendPaddedString(sb, parts.Comment, mColStart[(int)Col.Label]); TextUtil.AppendPaddedString(sb, parts.Comment, mColStart[(int)Col.Label]);
break; break;
case LineListGen.Line.Type.VisualizationSet: case LineListGen.Line.Type.VisualizationSet:
break; // TODO(xyzzy) return; // show nothing
case LineListGen.Line.Type.Blank: case LineListGen.Line.Type.Blank:
break; break;
default: default:
@ -371,8 +382,8 @@ namespace SourceGen {
/// <summary> /// <summary>
/// Generates HTML output to the specified path. /// Generates HTML output to the specified path.
/// </summary> /// </summary>
/// <param name="pathName">Full pathname of output file. This defines the root /// <param name="pathName">Full pathname of output file (including ".html"). This
/// directory if there are additional files.</param> /// defines the root directory if there are additional files.</param>
/// <param name="overwriteCss">If set, existing CSS file will be replaced.</param> /// <param name="overwriteCss">If set, existing CSS file will be replaced.</param>
public void OutputToHtml(string pathName, bool overwriteCss) { public void OutputToHtml(string pathName, bool overwriteCss) {
string exportTemplate = RuntimeDataAccess.GetPathName(HTML_EXPORT_TEMPLATE); string exportTemplate = RuntimeDataAccess.GetPathName(HTML_EXPORT_TEMPLATE);
@ -388,6 +399,41 @@ namespace SourceGen {
return; 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, // Perform some quick substitutions. This could be done more efficiently,
// but we're only doing this on the template file, which should be small. // but we're only doing this on the template file, which should be small.
tmplStr = tmplStr.Replace("$ProjectName$", mProject.DataFileName); tmplStr = tmplStr.Replace("$ProjectName$", mProject.DataFileName);
@ -620,7 +666,16 @@ namespace SourceGen {
parts.Comment.Length); parts.Comment.Length);
break; break;
case LineListGen.Line.Type.VisualizationSet: 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: case LineListGen.Line.Type.Blank:
break; break;
default: default:
@ -631,6 +686,73 @@ namespace SourceGen {
tw.WriteLine(sb); tw.WriteLine(sb);
} }
/// <summary>
/// Generate one or more GIF image files, and generate 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) {
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("&nbsp;");
}
sb.Append("<img class=\"vis\" alt=\"vis\" src=\"");
sb.Append(imageDirFileName);
sb.Append('/');
sb.Append(fileName);
sb.Append("\" width=\"" + thumbWidth + "\" height=\"" + thumbHeight + "\"/>");
}
}
/// <summary> /// <summary>
/// Appends a string to the string buffer. If the number of characters in the buffer /// 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 /// is less than the desired start position, spaces will be added. At least one space

View File

@ -957,6 +957,12 @@ namespace SourceGen {
mProject.GetAnattrib(offset - 1).IsData) { mProject.GetAnattrib(offset - 1).IsData) {
// Transition from data to code. (Don't add blank line for inline data.) // Transition from data to code. (Don't add blank line for inline data.)
lines.Add(GenerateBlankLine(offset)); 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) { } else if (addBlank) {
// Previous instruction wanted to be followed by a blank line. // Previous instruction wanted to be followed by a blank line.
lines.Add(GenerateBlankLine(offset)); lines.Add(GenerateBlankLine(offset));

View File

@ -2205,6 +2205,7 @@ namespace SourceGen {
Exporter eport = new Exporter(mProject, CodeLineList, mOutputFormatter, Exporter eport = new Exporter(mProject, CodeLineList, mOutputFormatter,
dlg.ColFlags, rightWidths); dlg.ColFlags, rightWidths);
eport.IncludeNotes = dlg.IncludeNotes; eport.IncludeNotes = dlg.IncludeNotes;
eport.GenerateImageFiles = dlg.GenerateImageFiles;
eport.LongLabelNewLine = dlg.LongLabelNewLine; eport.LongLabelNewLine = dlg.LongLabelNewLine;
if (dlg.SelectionOnly) { if (dlg.SelectionOnly) {
DisplayListSelection selection = mMainWin.CodeDisplayList.SelectedIndices; DisplayListSelection selection = mMainWin.CodeDisplayList.SelectedIndices;

View File

@ -56,6 +56,7 @@ limitations under the License.
<system:String x:Key="str_ErrBadTypeHint">Type hint not recognized</system:String> <system:String x:Key="str_ErrBadTypeHint">Type hint not recognized</system:String>
<system:String x:Key="str_ErrBadVisualizationFmt">Invalid visualization item: {0}</system:String> <system:String x:Key="str_ErrBadVisualizationFmt">Invalid visualization item: {0}</system:String>
<system:String x:Key="str_ErrBadVisualizationSetFmt">Invalid visualization set at +{0:x6}</system:String> <system:String x:Key="str_ErrBadVisualizationSetFmt">Invalid visualization set at +{0:x6}</system:String>
<system:String x:Key="str_ErrDirCreateFailedFmt">Unable to create directory {0}: {1}</system:String>
<system:String x:Key="str_ErrDuplicateLabelFmt">Removed duplicate label '{0}' (offset +{1:x6})</system:String> <system:String x:Key="str_ErrDuplicateLabelFmt">Removed duplicate label '{0}' (offset +{1:x6})</system:String>
<system:String x:Key="str_ErrFileCopyFailedFmt">Failed copying {0} to {1}: {2}.</system:String> <system:String x:Key="str_ErrFileCopyFailedFmt">Failed copying {0} to {1}: {2}.</system:String>
<system:String x:Key="str_ErrFileExistsNotDirFmt">The file {0} exists, but is not a directory.</system:String> <system:String x:Key="str_ErrFileExistsNotDirFmt">The file {0} exists, but is not a directory.</system:String>

View File

@ -93,6 +93,8 @@ namespace SourceGen.Res {
(string)Application.Current.FindResource("str_ErrBadVisualizationFmt"); (string)Application.Current.FindResource("str_ErrBadVisualizationFmt");
public static string ERR_BAD_VISUALIZATION_SET_FMT = public static string ERR_BAD_VISUALIZATION_SET_FMT =
(string)Application.Current.FindResource("str_ErrBadVisualizationSetFmt"); (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 = public static string ERR_DUPLICATE_LABEL_FMT =
(string)Application.Current.FindResource("str_ErrDuplicateLabelFmt"); (string)Application.Current.FindResource("str_ErrDuplicateLabelFmt");
public static string ERR_FILE_COPY_FAILED_FMT = public static string ERR_FILE_COPY_FAILED_FMT =

View File

@ -323,7 +323,7 @@ assembler.</p>
<p>The text output is similar to what you'd get by copying lines to the <p>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 clipboard and pasting them into a text file, except that you have greater
control over which columns are included. The HTML version is augmented control over which columns are included. The HTML version is augmented
with links.</p> with links and (optionally) images.</p>
<p>Use File &gt; Export to open the export dialog. You have several <p>Use File &gt; Export to open the export dialog. You have several
options:</p> options:</p>
@ -346,6 +346,8 @@ options:</p>
plain text and Comma-Separated Value format. The latter is useful plain text and Comma-Separated Value format. The latter is useful
for importing source code into another application, such as a for importing source code into another application, such as a
spreadsheet.</li> spreadsheet.</li>
<li><b>Generate image files</b>. When exporting to HTML, selecting this
will cause GIF images to be generated for visualizations.</li>
<li><b>Overwrite CSS file</b>. Some aspects of the HTML output's format <li><b>Overwrite CSS file</b>. Some aspects of the HTML output's format
are defined by a file called "SGStyle.css", which may be shared between are defined by a file called "SGStyle.css", which may be shared between
multiple HTML files and customized. The file is copied out multiple HTML files and customized. The file is copied out

View File

@ -23,4 +23,20 @@ a[href^="#"]:link, a[href^="#"]:visited {
a[href^="#"]:hover { a[href^="#"]:hover {
color: blue; color: blue;
text-decoration: underline; text-decoration: underline;
} }
/*
* 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;
}

View File

@ -19,7 +19,10 @@
"Comments":{ "Comments":{
}, },
"LongComments":{ "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":{ "Notes":{
"222":{ "222":{
"Text":"Bitmaps here","BoxMode":false,"MaxWidth":80,"BackgroundColor":-256}}, "Text":"Bitmaps here","BoxMode":false,"MaxWidth":80,"BackgroundColor":-256}},
@ -43,6 +46,19 @@
"LvTables":{ "LvTables":{
}, },
"VisualizationSets":{ "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":{ "222":{
"Items":[{ "Items":[{
"Tag":"vis0000de","VisGenIdent":"apple2-hi-res-bitmap","VisGenParams":{ "Tag":"vis0000de","VisGenIdent":"apple2-hi-res-bitmap","VisGenParams":{

View File

@ -79,6 +79,7 @@ namespace SourceGen.Sandbox {
/// Prepares the plugin directory. Creates it and copies PluginCommon.dll in. /// Prepares the plugin directory. Creates it and copies PluginCommon.dll in.
/// Throws an exception if something fails. /// Throws an exception if something fails.
/// </summary> /// </summary>
/// <exception cref="IOException">Various failures from file/dir operations.</exception>
public static void PreparePluginDir() { public static void PreparePluginDir() {
string dstDir = GetPluginDirPath(); string dstDir = GetPluginDirPath();
if (File.Exists(dstDir) && !Directory.Exists(dstDir)) { if (File.Exists(dstDir) && !Directory.Exists(dstDir)) {

View File

@ -47,7 +47,8 @@ namespace SourceGen {
public ReadOnlyDictionary<string, object> VisGenParams { get; private set; } public ReadOnlyDictionary<string, object> VisGenParams { get; private set; }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Because the underlying data never changes, we only need to regenerate the /// Because the underlying data never changes, we only need to regenerate the

View File

@ -102,6 +102,7 @@ limitations under the License.
<GroupBox Grid.Row="2" Header="HTML Options" Padding="2,4"> <GroupBox Grid.Row="2" Header="HTML Options" Padding="2,4">
<StackPanel> <StackPanel>
<CheckBox Content="Generate image files" Margin="0,2,0,0" IsChecked="{Binding GenerateImageFiles}"/>
<CheckBox Content="Overwrite CSS file" Margin="0,2,0,0" IsChecked="{Binding OverwriteCss}"/> <CheckBox Content="Overwrite CSS file" Margin="0,2,0,0" IsChecked="{Binding OverwriteCss}"/>
</StackPanel> </StackPanel>
</GroupBox> </GroupBox>

View File

@ -165,6 +165,12 @@ namespace SourceGen.WpfGui {
set { mOverwriteCss = value; OnPropertyChanged(); } set { mOverwriteCss = value; OnPropertyChanged(); }
} }
private bool mGenerateImageFiles;
public bool GenerateImageFiles {
get { return mGenerateImageFiles; }
set { mGenerateImageFiles = value; OnPropertyChanged(); }
}
private enum TextMode { private enum TextMode {
Unknown = 0, Unknown = 0,
PlainText, PlainText,
@ -210,6 +216,8 @@ namespace SourceGen.WpfGui {
SelectionOnly = AppSettings.Global.GetBool(AppSettings.EXPORT_SELECTION_ONLY, false); SelectionOnly = AppSettings.Global.GetBool(AppSettings.EXPORT_SELECTION_ONLY, false);
LongLabelNewLine = LongLabelNewLine =
AppSettings.Global.GetBool(AppSettings.EXPORT_LONG_LABEL_NEW_LINE, false); 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 int[] colWidths = new int[] { 9, 8, 11, 72 }; // 100-col output
string colStr = AppSettings.Global.GetString(AppSettings.EXPORT_COL_WIDTHS, null); 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_SHOW_ATTR, ShowAttr);
AppSettings.Global.SetBool(AppSettings.EXPORT_SELECTION_ONLY, SelectionOnly); AppSettings.Global.SetBool(AppSettings.EXPORT_SELECTION_ONLY, SelectionOnly);
AppSettings.Global.SetBool(AppSettings.EXPORT_LONG_LABEL_NEW_LINE, LongLabelNewLine); AppSettings.Global.SetBool(AppSettings.EXPORT_LONG_LABEL_NEW_LINE, LongLabelNewLine);
AppSettings.Global.SetBool(AppSettings.EXPORT_GENERATE_IMAGE_FILES,GenerateImageFiles);
int[] colWidths = new int[] { int[] colWidths = new int[] {
AsmLabelColWidth, AsmOpcodeColWidth, AsmOperandColWidth, AsmCommentColWidth AsmLabelColWidth, AsmOpcodeColWidth, AsmOperandColWidth, AsmCommentColWidth
}; };