Skip to content

Use user-selected format in Tk savefig, rather than inferring it from the filename #24531

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions doc/api/next_api_changes/behavior/24531-DOS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Tk backend respects file format selection when saving figures
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

When saving a figure from a Tkinter GUI to a filename without an
extension, the file format is now selected based on the value of
the dropdown menu, rather than defaulting to PNG. When the filename
contains an extension, or the OS automatically appends one, the
behavior remains unchanged.
13 changes: 13 additions & 0 deletions doc/api/next_api_changes/development/24531-DOS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Increase to minimum supported optional dependencies
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

For Matplotlib 3.8, the :ref:`minimum supported versions of optional dependencies
<optional_dependencies>` are being bumped:

+------------+-----------------+---------------+
| Dependency | min in mpl3.7 | min in mpl3.8 |
+============+=================+===============+
| Tk | 8.4 | 8.5 |
+------------+-----------------+---------------+

This is consistent with our :ref:`min_deps_policy`
2 changes: 1 addition & 1 deletion doc/devel/dependencies.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Matplotlib figures can be rendered to various user interfaces. See
:ref:`what-is-a-backend` for more details on the optional Matplotlib backends
and the capabilities they provide.

* Tk_ (>= 8.4, != 8.6.0 or 8.6.1): for the Tk-based backends. Tk is part of
* Tk_ (>= 8.5, != 8.6.0 or 8.6.1): for the Tk-based backends. Tk is part of
most standard Python installations, but it's not part of Python itself and
thus may not be present in rare cases.
* PyQt6_ (>= 6.1), PySide6_, PyQt5_, or PySide2_: for the Qt-based backends.
Expand Down
41 changes: 25 additions & 16 deletions lib/matplotlib/backends/_backend_tk.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
import math
import os.path
import pathlib
import sys
import tkinter as tk
import tkinter.filedialog
Expand Down Expand Up @@ -499,11 +500,8 @@ def create_with_canvas(cls, canvas_class, figure, num):
'images/matplotlib_large.png'))
icon_img_large = ImageTk.PhotoImage(
file=icon_fname_large, master=window)
try:
window.iconphoto(False, icon_img_large, icon_img)
except Exception as exc:
# log the failure (due e.g. to Tk version), but carry on
_log.info('Could not load matplotlib icon: %s', exc)

window.iconphoto(False, icon_img_large, icon_img)

canvas = canvas_class(figure, master=window)
manager = cls(canvas, num, window)
Expand Down Expand Up @@ -846,15 +844,15 @@ def _Spacer(self):
return s

def save_figure(self, *args):
filetypes = self.canvas.get_supported_filetypes().copy()
default_filetype = self.canvas.get_default_filetype()
filetypes = self.canvas.get_supported_filetypes_grouped()
tk_filetypes = [
(name, " ".join(f"*.{ext}" for ext in exts))
for name, exts in sorted(filetypes.items())
]

# Tk doesn't provide a way to choose a default filetype,
# so we just have to put it first
default_filetype_name = filetypes.pop(default_filetype)
sorted_filetypes = ([(default_filetype, default_filetype_name)]
+ sorted(filetypes.items()))
tk_filetypes = [(name, '*.%s' % ext) for ext, name in sorted_filetypes]
default_extension = self.canvas.get_default_filetype()
default_filetype = self.canvas.get_supported_filetypes()[default_extension]
filetype_variable = tk.StringVar(self, default_filetype)

# adding a default extension seems to break the
# asksaveasfilename dialog when you choose various save types
Expand All @@ -863,14 +861,18 @@ def save_figure(self, *args):
# defaultextension = self.canvas.get_default_filetype()
defaultextension = ''
initialdir = os.path.expanduser(mpl.rcParams['savefig.directory'])
initialfile = self.canvas.get_default_filename()
# get_default_filename() contains the default extension. On some platforms,
# choosing a different extension from the dropdown does not overwrite it,
# so we need to remove it to make the dropdown functional.
initialfile = pathlib.Path(self.canvas.get_default_filename()).stem
fname = tkinter.filedialog.asksaveasfilename(
master=self.canvas.get_tk_widget().master,
title='Save the figure',
filetypes=tk_filetypes,
defaultextension=defaultextension,
initialdir=initialdir,
initialfile=initialfile,
typevariable=filetype_variable
)

if fname in ["", ()]:
Expand All @@ -879,9 +881,16 @@ def save_figure(self, *args):
if initialdir != "":
mpl.rcParams['savefig.directory'] = (
os.path.dirname(str(fname)))

# If the filename contains an extension, let savefig() infer the file
# format from that. If it does not, use the selected dropdown option.
if pathlib.Path(fname).suffix[1:] != "":
extension = None
else:
extension = filetypes[filetype_variable.get()][0]

try:
# This method will handle the delegation to the correct type
self.canvas.figure.savefig(fname)
self.canvas.figure.savefig(fname, format=extension)
except Exception as e:
tkinter.messagebox.showerror("Error saving file", str(e))

Expand Down