From e001d1a24ed9b1be995e58cca2fd986d47b5183d Mon Sep 17 00:00:00 2001 From: glopesdev Date: Thu, 9 Jun 2022 18:44:11 +0100 Subject: [PATCH 1/5] Refactor image export functions into static class --- Bonsai.Editor/EditorForm.cs | 33 +++++------------ Bonsai.Editor/WorkflowExporter.cs | 59 +++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 24 deletions(-) create mode 100644 Bonsai.Editor/WorkflowExporter.cs diff --git a/Bonsai.Editor/EditorForm.cs b/Bonsai.Editor/EditorForm.cs index b3d5e4ad7..c49e6ebaf 100644 --- a/Bonsai.Editor/EditorForm.cs +++ b/Bonsai.Editor/EditorForm.cs @@ -1022,13 +1022,10 @@ void ExportImage(WorkflowGraphView model, string fileName) if (model.GraphView.SelectedNodes.Count() > 0) { var selectedElements = model.GraphView.SelectedNodes.ToWorkflowBuilder(); - var selectedLayout = selectedElements.Workflow.ConnectedComponentLayering().ToList(); - using var graphView = new GraphViewControl - { - IconRenderer = model.GraphView.IconRenderer, - Font = model.GraphView.Font, - Nodes = selectedLayout - }; + using var graphView = WorkflowExporter.CreateGraphView( + selectedElements.Workflow, + model.GraphView.Font, + model.GraphView.IconRenderer); ExportImage(graphView, fileName); } else ExportImage(model.GraphView, fileName); @@ -1036,32 +1033,20 @@ void ExportImage(WorkflowGraphView model, string fileName) void ExportImage(GraphViewControl graphView, string fileName) { - var bounds = graphView.GetLayoutSize(); var extension = Path.GetExtension(fileName); if (extension == ".svg") { - var graphics = new SvgNet.SvgGdi.SvgGraphics(); - graphView.DrawGraphics(graphics, true); - var svg = graphics.WriteSVGString(); - var attributes = string.Format( - " Date: Mon, 13 Jun 2022 19:43:51 +0100 Subject: [PATCH 2/5] Ensure SVG is exported at default DPI scale --- Bonsai.Editor/EditorForm.cs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/Bonsai.Editor/EditorForm.cs b/Bonsai.Editor/EditorForm.cs index c49e6ebaf..d1ab0df73 100644 --- a/Bonsai.Editor/EditorForm.cs +++ b/Bonsai.Editor/EditorForm.cs @@ -1019,29 +1019,23 @@ void ExportImage(WorkflowGraphView model) void ExportImage(WorkflowGraphView model, string fileName) { + var workflow = model.Workflow; if (model.GraphView.SelectedNodes.Count() > 0) { var selectedElements = model.GraphView.SelectedNodes.ToWorkflowBuilder(); - using var graphView = WorkflowExporter.CreateGraphView( - selectedElements.Workflow, - model.GraphView.Font, - model.GraphView.IconRenderer); - ExportImage(graphView, fileName); + workflow = selectedElements.Workflow; } - else ExportImage(model.GraphView, fileName); - } - void ExportImage(GraphViewControl graphView, string fileName) - { var extension = Path.GetExtension(fileName); if (extension == ".svg") { - var svg = WorkflowExporter.ExportSvg(graphView); + using var font = new Font(Font.FontFamily, Font.SizeInPoints * inverseScaleFactor.Height); + var svg = WorkflowExporter.ExportSvg(workflow, font, iconRenderer); File.WriteAllText(fileName, svg); } else { - using var bitmap = WorkflowExporter.ExportBitmap(graphView); + using var bitmap = WorkflowExporter.ExportBitmap(workflow, Font, iconRenderer); if (string.IsNullOrEmpty(fileName)) { Clipboard.SetImage(bitmap); From 01e0518256574200bcb1787c60b67e10f54dda63 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Mon, 20 Jun 2022 20:00:16 +0100 Subject: [PATCH 3/5] Ensure SVG export uses fixed DPI measurements --- Bonsai.Editor/EditorForm.cs | 3 +- Bonsai.Editor/GraphView/GraphViewControl.cs | 39 ++++++++++----------- Bonsai.Editor/WorkflowExporter.cs | 24 +++++++++---- 3 files changed, 37 insertions(+), 29 deletions(-) diff --git a/Bonsai.Editor/EditorForm.cs b/Bonsai.Editor/EditorForm.cs index d1ab0df73..171e130f2 100644 --- a/Bonsai.Editor/EditorForm.cs +++ b/Bonsai.Editor/EditorForm.cs @@ -1029,8 +1029,7 @@ void ExportImage(WorkflowGraphView model, string fileName) var extension = Path.GetExtension(fileName); if (extension == ".svg") { - using var font = new Font(Font.FontFamily, Font.SizeInPoints * inverseScaleFactor.Height); - var svg = WorkflowExporter.ExportSvg(workflow, font, iconRenderer); + var svg = WorkflowExporter.ExportSvg(workflow, iconRenderer); File.WriteAllText(fileName, svg); } else diff --git a/Bonsai.Editor/GraphView/GraphViewControl.cs b/Bonsai.Editor/GraphView/GraphViewControl.cs index 538fd44eb..8685907b2 100644 --- a/Bonsai.Editor/GraphView/GraphViewControl.cs +++ b/Bonsai.Editor/GraphView/GraphViewControl.cs @@ -66,7 +66,6 @@ partial class GraphViewControl : UserControl, IGraphView Pen SolidPen; Pen DashPen; Font DefaultIconFont; - Font ExportFont; float drawScale; bool ignoreMouseUp; @@ -238,6 +237,8 @@ public event EventHandler SelectedNodeChanged public SvgRendererFactory IconRenderer { get; set; } + public Image GraphicsProvider { get; set; } + public IEnumerable Nodes { get { return nodes; } @@ -310,6 +311,11 @@ public IEnumerable SelectedNodes } } + Graphics CreateVectorGraphics() + { + return GraphicsProvider != null ? Graphics.FromImage(GraphicsProvider) : CreateGraphics(); + } + void UpdateSelection(Action update) { InvalidateSelection(); @@ -342,12 +348,6 @@ void DisposeDrawResources() DashPen.Dispose(); SolidPen = DashPen = null; } - - if (ExportFont != null) - { - ExportFont.Dispose(); - ExportFont = null; - } } void UpdateCursorPen() @@ -372,9 +372,13 @@ void UpdateCursorPen() protected override void ScaleControl(SizeF factor, BoundsSpecified specified) { DisposeDrawResources(); - using (var graphics = CreateGraphics()) + using (var graphics = CreateVectorGraphics()) { - drawScale = graphics.DpiY / DefaultDpi * Font.SizeInPoints / Control.DefaultFont.SizeInPoints; + drawScale = graphics.DpiY / DefaultDpi; + if (GraphicsProvider == null) + { + drawScale *= Font.SizeInPoints / DefaultFont.SizeInPoints; + } } iconRendererState.Scale = drawScale; @@ -407,7 +411,7 @@ Rectangle GetBoundingRectangle(GraphNode node) var labelRectangle = nodeLayout.LabelRectangle; if (nodeText != nodeLayout.Text) { - using (var graphics = CreateGraphics()) + using (var graphics = CreateVectorGraphics()) { nodeLayout.SetNodeLabel(nodeText, Font, graphics); labelRectangle = RectangleF.Union(labelRectangle, nodeLayout.LabelRectangle); @@ -809,7 +813,7 @@ private void UpdateModelLayout() var size = SizeF.Empty; if (model != null) { - using (var graphics = CreateGraphics()) + using (var graphics = CreateVectorGraphics()) { var layerCount = model.Count(); foreach (var layer in model) @@ -1122,21 +1126,14 @@ private void DrawEdges(IGraphics graphics, Size offset) } } - public void DrawGraphics(IGraphics graphics, bool scaleFont) + public void DrawGraphics(IGraphics graphics) { graphics.SmoothingMode = SmoothingMode.AntiAlias; graphics.Clear(Color.White); - using (var measureGraphics = CreateGraphics()) + using (var measureGraphics = CreateVectorGraphics()) { var font = Font; - var fontScale = measureGraphics.DpiY / DefaultDpi; - if (scaleFont && fontScale != 1.0) - { - ExportFont = ExportFont ?? new Font(Font.FontFamily, Font.SizeInPoints * fontScale); - font = ExportFont; - } - using (var fill = new SolidBrush(Color.White)) using (var stroke = new Pen(NodeEdgeColor, PenWidth)) { @@ -1152,7 +1149,7 @@ public void DrawGraphics(IGraphics graphics, bool scaleFont) { int charactersFitted, linesFilled; var size = measureGraphics.MeasureString( - line, Font, + line, font, labelRect.Size, VectorTextFormat, out charactersFitted, out linesFilled); diff --git a/Bonsai.Editor/WorkflowExporter.cs b/Bonsai.Editor/WorkflowExporter.cs index c917e191c..9e94753f2 100644 --- a/Bonsai.Editor/WorkflowExporter.cs +++ b/Bonsai.Editor/WorkflowExporter.cs @@ -1,5 +1,6 @@ using System.Drawing; using System.Linq; +using System.Windows.Forms; using Bonsai.Editor.GraphModel; using Bonsai.Editor.GraphView; using Bonsai.Expressions; @@ -8,20 +9,31 @@ namespace Bonsai.Editor { static class WorkflowExporter { - public static GraphViewControl CreateGraphView(ExpressionBuilderGraph workflow, Font font, SvgRendererFactory iconRenderer) + const float ReferenceDpi = 96f; + const float ReferenceFontSize = 8.25f; + static readonly Font SvgFont = new Font(Control.DefaultFont.FontFamily, ReferenceFontSize); + + static GraphViewControl CreateGraphView( + ExpressionBuilderGraph workflow, + Font font, + SvgRendererFactory iconRenderer, + Image graphicsProvider) { var selectedLayout = workflow.ConnectedComponentLayering().ToList(); return new GraphViewControl { + GraphicsProvider = graphicsProvider, Font = font, IconRenderer = iconRenderer, Nodes = selectedLayout }; } - public static string ExportSvg(ExpressionBuilderGraph workflow, Font font, SvgRendererFactory iconRenderer) + public static string ExportSvg(ExpressionBuilderGraph workflow, SvgRendererFactory iconRenderer) { - using var graphView = CreateGraphView(workflow, font, iconRenderer); + using var graphicsProvider = new Bitmap(1, 1); + graphicsProvider.SetResolution(ReferenceDpi, ReferenceDpi); + using var graphView = CreateGraphView(workflow, SvgFont, iconRenderer, graphicsProvider); return ExportSvg(graphView); } @@ -29,7 +41,7 @@ public static string ExportSvg(GraphViewControl graphView) { var bounds = graphView.GetLayoutSize(); var graphics = new SvgNet.SvgGdi.SvgGraphics(); - graphView.DrawGraphics(graphics, scaleFont: true); + graphView.DrawGraphics(graphics); var svg = graphics.WriteSVGString(); var attributes = string.Format( " Date: Tue, 21 Jun 2022 23:05:21 +0100 Subject: [PATCH 4/5] Rename export helper class --- Bonsai.Editor/EditorForm.cs | 4 ++-- Bonsai.Editor/{WorkflowExporter.cs => ExportHelper.cs} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename Bonsai.Editor/{WorkflowExporter.cs => ExportHelper.cs} (98%) diff --git a/Bonsai.Editor/EditorForm.cs b/Bonsai.Editor/EditorForm.cs index 171e130f2..0a5b6b8d7 100644 --- a/Bonsai.Editor/EditorForm.cs +++ b/Bonsai.Editor/EditorForm.cs @@ -1029,12 +1029,12 @@ void ExportImage(WorkflowGraphView model, string fileName) var extension = Path.GetExtension(fileName); if (extension == ".svg") { - var svg = WorkflowExporter.ExportSvg(workflow, iconRenderer); + var svg = ExportHelper.ExportSvg(workflow, iconRenderer); File.WriteAllText(fileName, svg); } else { - using var bitmap = WorkflowExporter.ExportBitmap(workflow, Font, iconRenderer); + using var bitmap = ExportHelper.ExportBitmap(workflow, Font, iconRenderer); if (string.IsNullOrEmpty(fileName)) { Clipboard.SetImage(bitmap); diff --git a/Bonsai.Editor/WorkflowExporter.cs b/Bonsai.Editor/ExportHelper.cs similarity index 98% rename from Bonsai.Editor/WorkflowExporter.cs rename to Bonsai.Editor/ExportHelper.cs index 9e94753f2..c60d45561 100644 --- a/Bonsai.Editor/WorkflowExporter.cs +++ b/Bonsai.Editor/ExportHelper.cs @@ -7,7 +7,7 @@ namespace Bonsai.Editor { - static class WorkflowExporter + static class ExportHelper { const float ReferenceDpi = 96f; const float ReferenceFontSize = 8.25f; From c37ea5a1e1af29ffc9c2c56277226697d56f91db Mon Sep 17 00:00:00 2001 From: glopesdev Date: Wed, 22 Jun 2022 10:43:23 +0100 Subject: [PATCH 5/5] Add support for exporting workflow as an image --- Bonsai.Editor/WorkflowExporter.cs | 51 +++++++++++++++++++++++++++++++ Bonsai/Launcher.cs | 21 +++++++++++++ Bonsai/Program.cs | 5 +++ 3 files changed, 77 insertions(+) create mode 100644 Bonsai.Editor/WorkflowExporter.cs diff --git a/Bonsai.Editor/WorkflowExporter.cs b/Bonsai.Editor/WorkflowExporter.cs new file mode 100644 index 000000000..b495eaa4c --- /dev/null +++ b/Bonsai.Editor/WorkflowExporter.cs @@ -0,0 +1,51 @@ +using System; +using System.IO; +using System.Windows.Forms; +using System.Xml; +using System.Xml.Serialization; +using Bonsai.Editor.GraphView; + +namespace Bonsai.Editor +{ + public static class WorkflowExporter + { + public static void ExportImage(string fileName, string imageFileName) + { + if (string.IsNullOrEmpty(fileName)) + { + throw new ArgumentNullException(nameof(fileName)); + } + + if (!File.Exists(fileName)) + { + throw new ArgumentException("Specified workflow file does not exist.", nameof(fileName)); + } + + if (string.IsNullOrEmpty(imageFileName)) + { + throw new ArgumentException("No output image file is specified.", nameof(imageFileName)); + } + + WorkflowBuilder workflowBuilder; + using (var reader = XmlReader.Create(fileName)) + { + reader.MoveToContent(); + var serializer = new XmlSerializer(typeof(WorkflowBuilder), reader.NamespaceURI); + workflowBuilder = (WorkflowBuilder)serializer.Deserialize(reader); + } + + var iconRenderer = new SvgRendererFactory(); + var extension = Path.GetExtension(imageFileName); + if (extension == ".svg") + { + var svg = ExportHelper.ExportSvg(workflowBuilder.Workflow, iconRenderer); + File.WriteAllText(imageFileName, svg); + } + else + { + using var bitmap = ExportHelper.ExportBitmap(workflowBuilder.Workflow, Control.DefaultFont, iconRenderer); + bitmap.Save(imageFileName); + } + } + } +} diff --git a/Bonsai/Launcher.cs b/Bonsai/Launcher.cs index 81526e26b..1408d2b37 100644 --- a/Bonsai/Launcher.cs +++ b/Bonsai/Launcher.cs @@ -141,6 +141,27 @@ internal static int LaunchWorkflowPlayer( return Program.NormalExitCode; } + internal static int LaunchExportImage( + string fileName, + string imageFileName, + PackageConfiguration packageConfiguration) + { + if (string.IsNullOrEmpty(fileName)) + { + Console.WriteLine("No workflow file was specified."); + return Program.NormalExitCode; + } + + if (string.IsNullOrEmpty(imageFileName)) + { + Console.WriteLine("No image file was specified."); + return Program.NormalExitCode; + } + + WorkflowExporter.ExportImage(fileName, imageFileName); + return Program.NormalExitCode; + } + static int ShowManifestReadError(string path, string message) { MessageBox.Show( diff --git a/Bonsai/Program.cs b/Bonsai/Program.cs index ca8a04879..1d6351765 100644 --- a/Bonsai/Program.cs +++ b/Bonsai/Program.cs @@ -24,6 +24,7 @@ static class Program const string PackageManagerCommand = "--package-manager"; const string PackageManagerUpdates = "updates"; const string ExportPackageCommand = "--export-package"; + const string ExportImageCommand = "--export-image"; const string ReloadEditorCommand = "--reload-editor"; const string GalleryCommand = "--gallery"; const string EditorDomainName = "EditorDomain"; @@ -45,9 +46,11 @@ internal static int Main(string[] args) var launchEditor = true; var debugScripts = false; var editorScale = 1.0f; + var exportImage = false; var updatePackages = false; var launchResult = default(EditorResult); var initialFileName = default(string); + var imageFileName = default(string); var layoutPath = default(string); var libFolders = new List(); var propertyAssignments = new Dictionary(); @@ -59,6 +62,7 @@ internal static int Main(string[] args) parser.RegisterCommand(DebugScriptCommand, () => debugScripts = true); parser.RegisterCommand(SuppressBootstrapCommand, () => bootstrap = false); parser.RegisterCommand(SuppressEditorCommand, () => launchEditor = false); + parser.RegisterCommand(ExportImageCommand, fileName => { imageFileName = fileName; exportImage = true; }); parser.RegisterCommand(ExportPackageCommand, () => { launchResult = EditorResult.ExportPackage; bootstrap = false; }); parser.RegisterCommand(ReloadEditorCommand, () => { launchResult = EditorResult.ReloadEditor; bootstrap = false; }); parser.RegisterCommand(GalleryCommand, () => { launchResult = EditorResult.OpenGallery; bootstrap = false; }); @@ -143,6 +147,7 @@ internal static int Main(string[] args) libFolders.ForEach(path => ConfigurationHelper.RegisterPath(packageConfiguration, path)); using var scriptExtensions = ScriptExtensionsProvider.CompileAssembly(Launcher.ProjectFramework, packageConfiguration, editorRepositoryPath, debugScripts); ConfigurationHelper.SetAssemblyResolve(packageConfiguration); + if (exportImage) return Launcher.LaunchExportImage(initialFileName, imageFileName, packageConfiguration); if (!launchEditor) return Launcher.LaunchWorkflowPlayer(initialFileName, layoutPath, packageConfiguration, propertyAssignments); else return Launcher.LaunchWorkflowEditor( packageConfiguration,