Skip to content
Advertisement

How to save a tkinter canvas as an image

I would like to save my drawing on a tkinter canvas as an image so I can open it for later use. I currently use this save system from this post however this is not a good way for me. First I would need to add an offset and second if i set the application so only some part of the canvas is actually visible, the part where the canvas is not visible appears black when saving the image.

hi

only part of the canvas is actually visible. If I open the saved image this is what it looks likeenter image description here only what was visible is actually there(the entire image was yellow before saving it).

The code of saving the image.

def save(widget(canvas), filelocation):
    x=root.winfo_rootx()+widget.winfo_x() + 74
    y=root.winfo_rooty()+widget.winfo_y() + 109
    x1=x+widget.winfo_width()
    y1=y+widget.winfo_height()
    ImageGrab.grab().crop((x,y,x1,y1)).save(filelocation)

Idea

After reading from this post it explains i could recreate all the stuff i drew on the canvas. So my idea is to put all the stuff i drew on the canvas such as lines i created on an invisible layer and paste it on the image. However i dont know if this is possible(may be possible with PIL, numpy or cv2)

Code(Is minimal reproducable)

import tkinter as tk
from tkinter import colorchooser, Canvas, N
from tkinter.ttk import *
from PIL import Image, ImageTk, ImageGrab
import keyboard

def save(widget, filelocation):
    x=root.winfo_rootx()+widget.winfo_x()
    y=root.winfo_rooty()+widget.winfo_y()
    x1=x+widget.winfo_width()
    y1=y+widget.winfo_height()
    ImageGrab.grab().crop((x,y,x1,y1)).save(filelocation)

def type_of(color):
    type_pen = 'marker'
    if type_pen == 'marker':
        pencil_motion_marker(color = color)

#pixel pen
def pencil_motion_marker(color):
    stage.bind('<Button-1>', get_pos_marker)
    stage.bind('<B1-Motion>', lambda event, color = color: pencil_draw_marker(event, color))

def get_pos_marker(event):
    global lastx, lasty
    
    lastx, lasty = event.x, event.y

def pencil_draw_marker(event, color):
    stage.create_line((lastx, lasty, event.x, event.y), width = width.get(), fill = color, capstyle = 'round')
    get_pos_marker(event)

def choose_pen_color():
    pencilcolor = colorchooser.askcolor(title = 'Pencil Color')
    type_of(pencilcolor[1])

##
        
def pencil_click():
    global width, opacity

    Whitepencolb = Button(optionsframe, text = 'Whitepencolimg', style = 'COLBG.TButton', command = lambda m = 'White': type_of(m))
    Whitepencolb.grid(row = 0, column = 0, padx = 10, pady = 1)
    
    Redpencolb = Button(optionsframe, text = 'Redpencolimg', style = 'COLBG.TButton', command = lambda m = 'Red': type_of(m))
    Redpencolb.grid(row = 1, column = 0, padx = 10, pady = 1)
    
    Magentapencolb = Button(optionsframe, text = 'Magentapencolimg', style = 'COLBG.TButton', command = lambda m = 'Magenta': type_of(m))
    Magentapencolb.grid(row = 0, column = 1, padx = 10, pady = 1)

    Limegreenpencolb = Button(optionsframe, text = 'Limegreenpencolimg', style = 'COLBG.TButton', command = lambda m = 'Lime': type_of(m))
    Limegreenpencolb.grid(row = 1, column = 1, padx = 10, pady = 1)
    
    Greenpencolb = Button(optionsframe, text = 'Greenpencolimg', style = 'COLBG.TButton', command = lambda m = 'Green': type_of(m))
    Greenpencolb.grid(row = 0, column = 2, padx = 10, pady = 1)
    
    Bluepencolb = Button(optionsframe, text = 'Bluepencolimg', style = 'COLBG.TButton', command = lambda m = 'Blue': type_of(m))
    Bluepencolb.grid(row = 1, column = 2, padx = 10, pady = 1)
    
    Cyanpencolb = Button(optionsframe, text = 'Cyanpencolimg', style = 'COLBG.TButton', command = lambda m = 'Cyan': type_of(m))
    Cyanpencolb.grid(row = 0, column = 3, padx = 10, pady = 1)
    
    Yellowpencolb = Button(optionsframe, text = 'Yellowpencolimg', style = 'COLBG.TButton', command = lambda m = 'Yellow': type_of(m))
    Yellowpencolb.grid(row = 1, column = 3, padx = 10, pady = 1)

    Orangepencolb = Button(optionsframe, text = 'Orangepencolimg', style = 'COLBG.TButton', command = lambda m = 'Orange': type_of(m))
    Orangepencolb.grid(row = 0, column = 4, padx = 10, pady = 1)

    Graypencolb = Button(optionsframe, text = 'Graypencolimg', style = 'COLBG.TButton', command = lambda m = 'Gray': type_of(m))
    Graypencolb.grid(row = 1, column = 4, padx = 10, pady = 1)

    Blackpencolb = Button(optionsframe, text = 'Blackpencolimg', style = 'COLBG.TButton', command = lambda m = 'Black': type_of(m))
    Blackpencolb.grid(row = 0, column = 5, padx = 10, pady = 1)

    Createnewpencolb = Button(optionsframe, text = 'Createnewpencolimg', style = 'COLBG.TButton', command = choose_pen_color)
    Createnewpencolb.grid(row = 1, column = 5, padx = 10, pady = 1)

    widthlabel = Label(optionsframe, text = 'Width: ', style = 'LABELBG.TLabel')
    width = Scale(optionsframe, from_ = 1, to = 20, style = 'SCALEBG.Horizontal.TScale')
    widthlabel.grid(row = 0, column = 6)
    width.grid(row = 0, column = 7)
    width.set(20)

    opacitylabel = Label(optionsframe, text = 'Opacity: ', style = 'LABELBG.TLabel')
    opacity = Scale(optionsframe, from_ = 0, to = 1.0, style = 'SCALEBG.Horizontal.TScale')
    opacitylabel.grid(row = 1, column = 6)
    opacity.grid(row = 1, column = 7)
    opacity.set(1.0)

def setup(filelocation):
    global stage, img_id, optionsframe, draw
    
    for widgets in root.winfo_children():
        widgets.destroy()

    root.config(bg = '#454545')
    iconsframewidth = int(screen_width / 20)
    
    frames = Style()
    frames.configure('FRAMES.TFrame', background = '#2a2a2a')
    sep = Style()
    sep.configure('SEP.TFrame', background = '#1a1a1a')
    style = Style()
    style.configure('STAGE.TFrame', background = '#454545')
    icon = Style()
    icon.configure('ICON.TButton', background = '#2a2a2a', foreground = '#2a2a2a')
    
    iconsframe = Frame(root, width = iconsframewidth, style = 'FRAMES.TFrame')
    iconsframe.pack(side = 'left', expand = False, fill = 'y')
    iconsframe.pack_propagate(0)
    sep1frame = Frame(root, style = 'SEP.TFrame', width = 5)
    sep1frame.pack(side = 'left', expand = False, fill = 'y')
    optionsframe = Frame(root, style = 'FRAMES.TFrame', height = 100)
    optionsframe.pack(side = 'top', expand = False, fill = 'x')
    optionsframe.pack_propagate(0)
    sep2frame = Frame(root, style = 'SEP.TFrame', height = 5)
    sep2frame.pack(side = 'top', expand = False, fill = 'x')
    propertyframe = Frame(root, style = 'FRAMES.TFrame', width = 150)
    propertyframe.pack(side = 'right', expand = False, fill = 'y')
    propertyframe.pack_propagate(0)
    sep3frame = Frame(root, style = 'SEP.TFrame', width = 5)
    sep3frame.pack(side = 'right', expand = False, fill = 'y')
    stageframe = Frame(root, style = 'STAGE.TFrame')
    stageframe.pack(side = 'top', expand = True, fill = 'both')
    stageframe.pack_propagate(0)

    image = Image.open(filelocation)
    width, height = image.size

    stage = Canvas(stageframe, width = width, height = height)
    stage.pack(side="top", anchor = 'c', expand=True)

    root.update()

    keyboard.add_hotkey("ctrl+s", lambda widget = stage, filelocation = filelocation: save(widget, filelocation))

    pencilbutton = Button(iconsframe, text = 'pencilimg', command = pencil_click, style = 'ICON.TButton')
    pencilbutton.pack(anchor = N, pady = 10)

    imgtk = ImageTk.PhotoImage(Image.open(filelocation)) 
    img_id = stage.create_image(stage.winfo_width() / 2, stage.winfo_height() / 2, image = imgtk)
    stage.image = imgtk

root = tk.Tk()
root.title('App')

screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()

w = 1150
h = 600
x = (screen_width / 2) - (w / 2)
y = (screen_height / 2) - (h / 2)

root.geometry('%dx%d+%d+%d' % (w, h, x, y))
root.minsize(1150, 600)

setup('Test.png')
root.mainloop()

Image

enter image description here

Small Problem

Replying to @Claudio : I am using the screenshot technique for saving Canvas as an image to a file right now. I noticed that the saved canvas image looks like this at the cornerenter image description here and after saving and reopening the image it looks like thisenter image description here ( the border of the canvas increases the size of the canvas image ).

Update 2. June 2022: Small Problem Solved by the updated code provided in the accepted answer.

Advertisement

Answer

How to save a tkinter Canvas graphics as an image?

It seems to be a fact that tkinter doesn’t provide a direct method allowing to get an image of Canvas graphics for saving it to an image file. There are two ways around this problem requiring solely an import of the Python PIL module (Pillow).

One of this ways is to perform a screenshot of painting on the Canvas area which can be done using PIL.ImageGrab.grab() or any other of the various methods for performing (cropped) screenshots and saving them to an image file ( see e.g. Fast screenshot of a small part of the screen in Python for a Python screenshot module fast enough to allow to make a video of the progressing painting on the Canvas ).

Another way is to paint on a Python PIL image updating the tkinter Canvas with the modified PIL image saving it then to a file using the .save() method available for saving PIL image objects.

The code provided in the question works generally as expected if save() uses both the Frame (stageframe) and the Canvas (stage) widgets required for getting the right x,y values for cropping of the screenshot in case the Canvas is placed within a Frame AND if the bounding box for cropping the screenshot takes into account that tkinter Canvas widget size includes a Canvas border and a Canvas highlight-border.

The code below is the in the question provided code with some added comments and appropriate modifications. It doesn’t require the keyboard module and saves the by painting modified Canvas as image file by clicking on the most-left upper pencilbutton handled by the pencil_click() function. It provides both methods for saving the graphics of the tkinter Canvas to an image file. Select one of them by assigning appropriate value to the global method variable ( method = 'screenshot' or method = 'imagepaint' ):

# https://stackoverflow.com/questions/72459847/how-to-save-a-tkinter-canvas-as-an-image
from tkinter     import Tk, colorchooser, Canvas, N, PhotoImage
from tkinter.ttk import Style, Frame, Button, Label, Scale
from PIL import Image, ImageTk   # required to load images in tkinter
# method = 'screenshot' or 'imagepaint'
method = 'screenshot'
borderthickness_bd = 2
highlightthickness = 1
if method == 'imagepaint': 
    from PIL import ImageDraw  # required to draw on the image 
if method == 'screenshot': 
    from PIL import ImageGrab  # required for the screenshot
filelocation = 'Test.png'
savelocation = 'Test_.png'
def save(stageframe, stage, savelocation):
    if method == 'imagepaint': 
        global image
        image.save(savelocation)
    if method == 'screenshot':
        global borderthickness_bd,  highlightthickness
        brdt = borderthickness_bd + highlightthickness
        # +1 and -2 because of thicknesses of Canvas borders (bd-border and highlight-border):
        x=root.winfo_rootx()+stageframe.winfo_x()+stage.winfo_x() +1*brdt
        y=root.winfo_rooty()+stageframe.winfo_y()+stage.winfo_y() +1*brdt
        x1=x+stage.winfo_width() -2*brdt
        y1=y+stage.winfo_height()-2*brdt
        ImageGrab.grab().crop((x,y,x1,y1)).save(savelocation)

def type_of(color):
    type_pen     = 'marker'
    if type_pen == 'marker':
        pencil_motion_marker(color = color)

#pixel pen
def pencil_motion_marker(color):
    stage.bind('<Button-1>' , get_pos_marker)
    stage.bind('<B1-Motion>', lambda event, color = color: pencil_draw_marker(event, color))

def get_pos_marker(event):
    global lastx, lasty
    lastx, lasty = event.x, event.y

def pencil_draw_marker(event, color):
    global method, lastx, lasty, draw, image, img_id
    # print( (lastx, lasty, event.x, event.y), color, int(width.get()) )
    if method == 'screenshot': 
        stage.create_line((lastx, lasty, event.x, event.y), width = width.get(), fill = color, capstyle = 'round')
        get_pos_marker(event)
    if method == 'imagepaint':
        w12 = int(width.get()/2)
        draw.ellipse( (event.x-w12, event.y-w12, event.x+w12, event.y+w12), fill=color )
        imgtk  = ImageTk.PhotoImage(image)
        stage.itemconfig(img_id, image=imgtk)
        stage.image = imgtk

def choose_pen_color():
    pencilcolor = colorchooser.askcolor(title = 'Pencil Color')
    type_of(pencilcolor[1])

##
        
def pencil_click():
    
    global width, opacity, stageframe, stage, savelocation

    # imgToSave = stage.image                                      # gives a PhotoImage object
    # imgToSave._PhotoImage__photo.write("Test.gif", format='gif') # which can be saved, but ...
    #                                ^--- ... with no painting done on Canvas - only the image.

    save(stageframe, stage, savelocation)
    
    Whitepencolb = Button(optionsframe, text = 'Whitepencolimg', style = 'COLBG.TButton', command = lambda m = 'White': type_of(m))
    Whitepencolb.grid(row = 0, column = 0, padx = 10, pady = 1)
    
    Redpencolb = Button(optionsframe, text = 'Redpencolimg', style = 'COLBG.TButton', command = lambda m = 'Red': type_of(m))
    Redpencolb.grid(row = 1, column = 0, padx = 10, pady = 1)
    
    Magentapencolb = Button(optionsframe, text = 'Magentapencolimg', style = 'COLBG.TButton', command = lambda m = 'Magenta': type_of(m))
    Magentapencolb.grid(row = 0, column = 1, padx = 10, pady = 1)

    Limegreenpencolb = Button(optionsframe, text = 'Limegreenpencolimg', style = 'COLBG.TButton', command = lambda m = 'Lime': type_of(m))
    Limegreenpencolb.grid(row = 1, column = 1, padx = 10, pady = 1)
    
    Greenpencolb = Button(optionsframe, text = 'Greenpencolimg', style = 'COLBG.TButton', command = lambda m = 'Green': type_of(m))
    Greenpencolb.grid(row = 0, column = 2, padx = 10, pady = 1)
    
    Bluepencolb = Button(optionsframe, text = 'Bluepencolimg', style = 'COLBG.TButton', command = lambda m = 'Blue': type_of(m))
    Bluepencolb.grid(row = 1, column = 2, padx = 10, pady = 1)
    
    Cyanpencolb = Button(optionsframe, text = 'Cyanpencolimg', style = 'COLBG.TButton', command = lambda m = 'Cyan': type_of(m))
    Cyanpencolb.grid(row = 0, column = 3, padx = 10, pady = 1)
    
    Yellowpencolb = Button(optionsframe, text = 'Yellowpencolimg', style = 'COLBG.TButton', command = lambda m = 'Yellow': type_of(m))
    Yellowpencolb.grid(row = 1, column = 3, padx = 10, pady = 1)

    Orangepencolb = Button(optionsframe, text = 'Orangepencolimg', style = 'COLBG.TButton', command = lambda m = 'Orange': type_of(m))
    Orangepencolb.grid(row = 0, column = 4, padx = 10, pady = 1)

    Graypencolb = Button(optionsframe, text = 'Graypencolimg', style = 'COLBG.TButton', command = lambda m = 'Gray': type_of(m))
    Graypencolb.grid(row = 1, column = 4, padx = 10, pady = 1)

    Blackpencolb = Button(optionsframe, text = 'Blackpencolimg', style = 'COLBG.TButton', command = lambda m = 'Black': type_of(m))
    Blackpencolb.grid(row = 0, column = 5, padx = 10, pady = 1)

    Createnewpencolb = Button(optionsframe, text = 'Createnewpencolimg', style = 'COLBG.TButton', command = choose_pen_color)
    Createnewpencolb.grid(row = 1, column = 5, padx = 10, pady = 1)

    widthlabel = Label(optionsframe, text = 'Width: ', style = 'LABELBG.TLabel')
    width = Scale(optionsframe, from_ = 1, to = 100, style = 'SCALEBG.Horizontal.TScale')
    widthlabel.grid(row = 0, column = 6)
    width.grid(row = 0, column = 7)
    width.set(20)

    opacitylabel = Label(optionsframe, text = 'Opacity: ', style = 'LABELBG.TLabel')
    opacity = Scale(optionsframe, from_ = 0, to = 1.0, style = 'SCALEBG.Horizontal.TScale')
    opacitylabel.grid(row = 1, column = 6)
    opacity.grid(row = 1, column = 7)
    opacity.set(1.0)

def setup(filelocation):
    global stage, stageframe, img_id, optionsframe, draw, image, img_id, method
    global borderthickness_bd, highlightthickness
    
    for widgets in root.winfo_children():
        widgets.destroy()

    root.config(bg = '#454545')
    iconsframewidth = int(screen_width / 20)
    
    frames = Style()
    frames.configure('FRAMES.TFrame', background = '#2a2a2a')
    sep = Style()
    sep.configure('SEP.TFrame', background = '#1a1a1a')
    style = Style()
    style.configure('STAGE.TFrame', background = '#454545')
    icon = Style()
    icon.configure('ICON.TButton', background = '#2a2a2a', foreground = '#2a2a2a')
    
    iconsframe = Frame(root, width = iconsframewidth, style = 'FRAMES.TFrame')
    iconsframe.pack(side = 'left', expand = False, fill = 'y')
    iconsframe.pack_propagate(0)
    sep1frame = Frame(root, style = 'SEP.TFrame', width = 5)
    sep1frame.pack(side = 'left', expand = False, fill = 'y')
    optionsframe = Frame(root, style = 'FRAMES.TFrame', height = 100)
    optionsframe.pack(side = 'top', expand = False, fill = 'x')
    optionsframe.pack_propagate(0)
    sep2frame = Frame(root, style = 'SEP.TFrame', height = 5)
    sep2frame.pack(side = 'top', expand = False, fill = 'x')
    propertyframe = Frame(root, style = 'FRAMES.TFrame', width = 150)
    propertyframe.pack(side = 'right', expand = False, fill = 'y')
    propertyframe.pack_propagate(0)
    sep3frame = Frame(root, style = 'SEP.TFrame', width = 5)
    sep3frame.pack(side = 'right', expand = False, fill = 'y')
    stageframe = Frame(root, style = 'STAGE.TFrame')
    stageframe.pack(side = 'top', expand = True, fill = 'both')
    stageframe.pack_propagate(0)

    image = Image.open(filelocation)
    width, height = image.size
    if method == 'imagepaint': 
        draw = ImageDraw.Draw(image)

    imgtk  = ImageTk.PhotoImage(image)
    # width, height = imgtk._PhotoImage__size
    
    # imgtk  = PhotoImage(filelocation)
    #   ^--- no width, hight information ???
    
    stage = Canvas(stageframe, width = width, height = height, bd=borderthickness_bd, highlightthickness=highlightthickness) # default: bd=2, highlightthickness=1
    stage.pack(side="top", anchor = 'c', expand=True)

    root.update()

    # keyboard.add_hotkey("ctrl+s", lambda widget = stageframe, filelocation = filelocation: save(widget, filelocation))

    pencilbutton = Button(iconsframe, text = 'pencilimg', command = pencil_click, style = 'ICON.TButton')
    pencilbutton.pack(anchor = N, pady = 10)

    img_id = stage.create_image(stage.winfo_width() / 2, stage.winfo_height() / 2, image = imgtk)
    stage.image = imgtk

root = Tk()
root.title('App')

screen_width  = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()

w = 1150
h =  600
x = (screen_width  / 2) - (w / 2)
y = (screen_height / 2) - (h / 2)

root.geometry('%dx%d+%d+%d' % (w, h, x, y))
root.minsize(1150, 600)

setup(filelocation)
root.mainloop()

Cropping a screenshot as a way of saving the graphics of tkinter Canvas is to be preferred over painting on a PIL image updating the tkinter Canvas because the latter has the side effect of slowing graphics down so painting smoothness suffer.

To see how to change the look of a button in tkinter (to change after the first click the pencilbutton to a savebutton), check out Python tkinter: error _tkinter.TclError: bad window path name “.!button2” for how it can be done.

User contributions licensed under: CC BY-SA
4 People found this is helpful
Advertisement