pythontkintercanvastkinter-canvassave-image

Saving content of tkinter canvas as image


Such a question has been posted many times before but I couldn't find a solution which would work with my code. I have a tkinter window with some plots (later the number will be dynamically) and a row and a column title. I need to save an image (preferably .png) from the content of the window. There are many solutions by making an image of a tkinter window by saving the content of a canvas which I tried many times but none of them worked for me. Either I got an empty .eps image or the conversion to a .png image didn't work. Also in principal a screenshot of a window can be done. I tried many suggestions already but the closest thing I received was an image whith half the window and half of the screen behind it. But I prefer to not do a screenshot because later the user should choose the quality of the image.

The code has a function which should create an .eps image which should be converted to a .png image but it doesn't work (Unable to locate Ghostscript on paths). But apart from what I tried I would appreciate any working code example embedded within my code.

P.S. Also maybe someone can tell me why the column title is so far away from the plots because I have no clue why it is.

import tkinter as tk
from matplotlib import pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from PIL import Image

# saving canvas as .png image
def save():
    fileName="Image"
    canvas.postscript(file=fileName + '.eps')
    # use PIL to convert to PNG
    img = Image.open(fileName + '.eps')
    img.save(fileName + '.png', 'png')

    canvas.update()
    canvas.postscript(file="file_name.ps", colormode='color')

# data for plots
cols = 2
x1, x2, x3, x4 = [1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3]
y1, y2, y3, y4 = [1, 2, 3], [1, 2, 1], [3, 2, 1], [3, 2, 3]

#
root = tk.Tk()
canvas = tk.Canvas(root, background="grey")
row_title = tk.Label(canvas, text="rows", font=("Arial 24 bold"), background="grey")
column_title = tk.Label(canvas, text="columns", font=("Arial 24 bold"), background="grey")

# plots
fig = plt.Figure(figsize=(5, 5))
fig.set_facecolor("grey")

plot1 = fig.add_subplot(221)
plot1.scatter(x1, y1)
plot2 = fig.add_subplot(222)
plot2.scatter(x2, y2)
plot3 = fig.add_subplot(223)
plot3.scatter(x3, y3)
plot4 = fig.add_subplot(224)
plot4.scatter(x4, y4)

chart1 = FigureCanvasTkAgg(fig, canvas)
chart1.get_tk_widget().grid(row=1, column=1)

button = tk.Button(text="Save", command=save)

canvas.grid(row=0, column=0)
row_title.grid(row=1, column=0, rowspan=2, padx=5, pady=5)
column_title.grid(row=0, column=1, columnspan=2)
button.grid(row=2, column=0)

root.mainloop()

Solution

  • This code works on Windows.

    import os
    import tkinter as tk
    from tkinter import filedialog as fido
    from PIL import ImageGrab
    
    # from matplotlib import pyplot as plt
    # from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
    
    # NOTE: This explanation concentrates on tkinter not matplotlib
    root = tk.Tk()
    
    # make root window and contents resizeable
    root.rowconfigure(0, weight = 1)
    root.columnconfigure(0, weight = 1)
    
    canvas = tk.Canvas(root, background = "grey")
    # 'sticky' in grid is necessary so that the canvas will expand to fill the available space.
    # Without it the canvas will wrap itself around the contents. (creating the smallest footprint)
    canvas.grid(sticky = tk.NSEW)
    
    # These labels are contained within the canvas so don't grid them
    row_title = tk.Label(
        canvas, text="rows", font=("Arial 24 bold"), background="grey")
    column_title = tk.Label(
        canvas, text="columns", font=("Arial 24 bold"), background="grey")
    
    # Instead, wrap each in a canvas window object.
    A = canvas.create_window(2, 200, window = row_title, anchor = tk.NW)
    # fiddle with coordinates until it is positioned correctly
    
    #
    # INSERT YOUR CHARTS HERE
    #
    
    # Now wrap 'column_title'
    B = canvas.create_window(200, 2, window = column_title, anchor = tk.NW)
    # Again, fiddle with coordinates until it is positioned correctly
    
    # Move and resize window as appropriate.
    
    # To save image press Escape key.
    
    # Function 'save' will take the entire visible canvas and save it as the
    # filetype determined by your filename.
    # The filename extension (.png, .gif, etc) determines the image format.
    # You may need to check PIL documentation for filetype specifics.
    
    def save(*event):
    
        filename = fido.asksaveasfilename(title = "Create Image")
        if filename:
            if os.path.exists(filename):
                print("Name already exists!")
            else:
                path, file = os.path.split(filename)
                name, ext = os.path.splitext(file)
                if ext.lower() in ['.gif', '.png']:
                    # Standard way of calculating widget dimensions
                    x1 = root.winfo_rootx() + canvas.winfo_x()
                    y1 = root.winfo_rooty() + canvas.winfo_y()
                    x2 = x1 + canvas.winfo_width()
                    y2 = y1 + canvas.winfo_height()
                    # Extract image
                    image = ImageGrab.grab().crop((x1, y1, x2, y2))
                    # And save it
                    image.save(filename)
                    print(f"Saved image {file}")
                else:
                    print("Unknown file type")
        else:
            print("Cancel")
    
    # Instead of a button i've used a key binding
    root.bind("<Escape>", save)
    
    root.mainloop()