add toolbar buttons copying the plot as PNG and SVG to the clipboard

Two new buttons below the plot copy the current figure to the
clipboard. The PNG copy renders off-screen on a temporary Agg canvas
at a fixed 12:8 inches and 800 px width, independent of the window
size, and restores figure canvas, size and dpi afterwards so the
on-screen plot is untouched. The RGBA buffer is wrapped in GLib.Bytes
for GdkPixbuf.Pixbuf.new_from_bytes, which current PyGObject requires
(fixes the 'Expected GLib.Bytes, but got bytes' error). The SVG copy
keeps using savefig into a text buffer; both report through logger.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
markusro committed 2026-10-19 23:16:34 +02:00
1 parent 0135f9b7a3
commit 1bed381639
2 files changed
+80 -33

No files matched your search

+32 -31
View File
@@ -253,6 +253,8 @@ class DamarisGUI:
"on_display_y_scaling_combobox_changed": self.monitor.display_scaling_changed,
"on_display_save_data_as_text_button_clicked": self.monitor.save_display_data_as_text,
"on_display_copy_data_to_clipboard_button_clicked": self.monitor.copy_display_data_to_clipboard,
"on_display_copy_png_button_clicked": self.monitor.copy_canvas_as_png,
"on_display_copy_svg_button_clicked": self.monitor.copy_canvas_as_svg,
"on_experiment_script_textview_event": self.sw.textviews_modified,
"on_main_notebook_switch_page": self.nopevent,
"main_window_close": self.nopevent
@@ -3832,25 +3834,20 @@ class MonitorWidgets:
return
try:
# Save the original figure size
original_size = self.matplot_figure.get_size_inches()
# Save figure to a buffer as SVG
import io as iomodule
buf = iomodule.StringIO()
self.matplot_figure.set_size_inches(6, 4)
self.matplot_figure.savefig(buf, format='svg', bbox_inches='tight')
buf.seek(0)
# Copy SVG text to clipboard
clipboard = gtk.Clipboard.get(gdk.SELECTION_CLIPBOARD)
clipboard.set_text(buf.read(), -1)
clipboard.store()
self.matplot_figure.set_size_inches(original_size)
log("Figure copied to clipboard as SVG")
logger.info("Figure copied to clipboard as SVG")
except Exception as e:
log(f"Error copying figure as SVG: {e}")
logger.error("Error copying figure as SVG: %s", e)
def copy_display_data_to_clipboard( self, widget, data=None ):
"""
@@ -3858,38 +3855,42 @@ class MonitorWidgets:
Also keep text data copy functionality as fallback
"""
# First, try to copy the matplotlib figure as image
# Save the original figure size
original_size = self.matplot_figure.get_size_inches()
if self.matplot_figure is not None and self.matplot_canvas is not None:
try:
# Save figure to a temporary buffer as PNG
import io as iomodule
buf = iomodule.BytesIO()
self.matplot_figure.set_size_inches(6, 4)
self.matplot_figure.savefig(buf, format='png', dpi=300, bbox_inches='tight')
buf.seek(0)
# Load the PNG data into a GdkPixbuf
from gi.repository import GdkPixbuf
loader = GdkPixbuf.PixbufLoader.new_with_type('png')
loader.write(buf.read())
loader.close()
pixbuf = loader.get_pixbuf()
# Render off-screen at a fixed size (12:8 inches, 800 px
# wide) on a temporary Agg canvas. Snapshotting the GTK
# canvas buffer instead would copy the window size, and
# re-rendering through the GTK canvas would replace its
# renderer and leave the on-screen plot blank.
from matplotlib.backends.backend_agg import FigureCanvasAgg
figure = self.matplot_figure
orig_canvas = figure.canvas
orig_size = figure.get_size_inches( )
orig_dpi = figure.dpi
try:
agg_canvas = FigureCanvasAgg( figure )
figure.set_size_inches( 12, 8 )
figure.dpi = 800 / 12
agg_canvas.draw( )
rgba = numpy.asarray( agg_canvas.buffer_rgba( ) )
finally:
figure.set_canvas( orig_canvas )
figure.set_size_inches( *orig_size )
figure.dpi = orig_dpi
height, width = rgba.shape[ :2 ]
pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(
glib.Bytes.new( rgba.tobytes( ) ), GdkPixbuf.Colorspace.RGB, True, 8,
width, height, width * 4 )
# Copy to clipboard
clipboard = gtk.Clipboard.get(gdk.SELECTION_CLIPBOARD)
clipboard.set_image(pixbuf)
clipboard.store()
# Restore original figure size
self.matplot_figure.set_size_inches(original_size)
# Redraw the canvas to ensure it stays visible
self.matplot_canvas.draw()
clipboard = gtk.Clipboard.get( gdk.SELECTION_CLIPBOARD )
clipboard.set_image( pixbuf )
log("Figure copied to clipboard as PNG image")
logger.info("Figure copied to clipboard as PNG image")
return
except Exception as e:
log("Error copying figure to clipboard: %s" % str(e))
logger.error("Error copying figure to clipboard: %s", e)
##################### functions to feed display
def clear_display( self ):
+48 -2
View File
@@ -1414,10 +1414,56 @@ Public License instead of this License.
</packing>
</child>
<child>
<placeholder/>
<object class="GtkButton" id="display_copy_png_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="tooltip-text" translatable="yes">Copy plot as PNG image to clipboard</property>
<signal name="clicked" handler="on_display_copy_png_button_clicked" swapped="no"/>
<child>
<object class="GtkLabel" id="label_copy_png">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="xpad">10</property>
<property name="label" translatable="yes">&lt;span foreground="#0000ff"&gt;PNG&lt;/span&gt;</property>
<property name="use-markup">True</property>
</object>
</child>
</object>
<packing>
<property name="left-attach">3</property>
<property name="right-attach">4</property>
<property name="top-attach">1</property>
<property name="bottom-attach">2</property>
<property name="x-options"/>
<property name="y-options">GTK_FILL</property>
</packing>
</child>
<child>
<placeholder/>
<object class="GtkButton" id="display_copy_svg_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="tooltip-text" translatable="yes">Copy plot as SVG to clipboard</property>
<signal name="clicked" handler="on_display_copy_svg_button_clicked" swapped="no"/>
<child>
<object class="GtkLabel" id="label_copy_svg">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="xpad">10</property>
<property name="label" translatable="yes">&lt;span foreground="#0000ff"&gt;SVG&lt;/span&gt;</property>
<property name="use-markup">True</property>
</object>
</child>
</object>
<packing>
<property name="left-attach">2</property>
<property name="right-attach">3</property>
<property name="top-attach">1</property>
<property name="bottom-attach">2</property>
<property name="x-options"/>
<property name="y-options">GTK_FILL</property>
</packing>
</child>
<child>
<placeholder/>