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.
only part of the canvas is actually visible. If I open the saved image this is what it looks like 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
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 corner and after saving and reopening the image it looks like this ( 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.