30
from Tkinter import *
root = Tk()
cv = Canvas(root)
cv.create_rectangle(10,10,50,50)
cv.pack()
root.mainloop()

I want to convert canvas content to a bitmap or other image, and then do other operations, such as rotating or scaling the image, or changing its coordinates.

Bitmaps can improve efficiency to show if I am no longer drawing.

What should I do?

Pops
  • 30,199
  • 37
  • 136
  • 151
liupeixin
  • 718
  • 1
  • 9
  • 15

7 Answers7

34

You can either generate a postscript document (to feed into some other tool: ImageMagick, Ghostscript, etc):

from Tkinter import *
root = Tk()
cv = Canvas(root)
cv.create_rectangle(10,10,50,50)
cv.pack()
root.mainloop()

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

root.mainloop()

or draw the same image in parallel on PIL and on Tkinter's canvas (see: Saving a Tkinter Canvas Drawing (Python)). For example (inspired by the same article):

from Tkinter import *
import Image, ImageDraw

width = 400
height = 300
center = height//2
white = (255, 255, 255)
green = (0,128,0)

root = Tk()

# Tkinter create a canvas to draw on
cv = Canvas(root, width=width, height=height, bg='white')
cv.pack()

# PIL create an empty image and draw object to draw on
# memory only, not visible
image1 = Image.new("RGB", (width, height), white)
draw = ImageDraw.Draw(image1)

# do the Tkinter canvas drawings (visible)
cv.create_line([0, center, width, center], fill='green')

# do the PIL image/draw (in memory) drawings
draw.line([0, center, width, center], green)

# PIL image can be saved as .png .jpg .gif or .bmp file (among others)
filename = "my_drawing.jpg"
image1.save(filename)

root.mainloop()
Nif
  • 598
  • 4
  • 7
33

I have found a great way of doing this which is really helpful. For it, you need the PIL module. Here is the code:

from PIL import ImageGrab

def getter(widget):
    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("file path here")

What this does is you pass a widget name into the function. The command root.winfo_rootx() and the root.winfo_rooty() get the pixel position of the top left of the overall root window.

Then, the widget.winfo_x() and widget.winfo_y() are added to, basically just get the pixel coordinate of the top left hand pixel of the widget which you want to capture (at pixels (x,y) of your screen).

I then find the (x1,y1) which is the bottom left pixel of the widget. The ImageGrab.grab() makes a printscreen, and I then crop it to only get the bit containing the widget. Although not perfect, and won't make the best possible image, this is a great tool for just getting a image of any widget and saving it.

If you have any questions, post a comment! Hope this helped!

martineau
  • 119,623
  • 25
  • 170
  • 301
B.Jenkins
  • 449
  • 4
  • 8
  • Hi, when I run the code it makes the picture too soon. Can you please post an example script with tkinter graphics and then saving it? Maybe I'm placing or calling out the function at the wrong time. – E. Muuli Jan 30 '17 at 16:57
  • @EerikMuuli You must only call the function when you want the picture taken. For example, you could get a button with a command, and in the function for the button's command, simply put `getter(x)` where x is the widget, or even the whole root window. It should be no trouble. If you have further problems just reply in a comment here. – B.Jenkins Jan 31 '17 at 20:11
  • Okay, I found out where the problem is - I will paste my code here. What the script now does is that it prints the canvas twice and runs into an error - although it should open it once and close it after 4.5 sec. It also prints out the "wut" 2 times which is really weird. Here's the code: http://pastebin.com/MPgABMEv – E. Muuli Feb 01 '17 at 19:14
  • 2
    @EerikMuuli Firstly, you are using pyscreenshot module instead of PIL, so I'm not entirely sure how this works. As PIL can do screen-shots and lots of other functions, I would simply get the PIL module instead. Also, it looks like you just want to make the canvas, and them make a picture of a rectangle. There are simpler ways to do this! To do it your way, then use PIL, and the save function that I used in my answer, and if you really want to exit after you have taken the screenshot, then just put an exit() or preferably root.destroy() command in the getter function definition. – B.Jenkins Feb 03 '17 at 17:03
  • 2
    Marvelous approach! Much better than trying to repeat all the drawing commands on a blank PIL image or creating Postscipt output and using another tool to convert it into an image. Note that Instead of capturing the whole screen and then cropping it, this can be done in a single step with `ImageGrab.grab((x,y,x1,y1)).save("file path here")` (see [`ImageGrab.grab()`](http://pillow.readthedocs.io/en/4.1.x/reference/ImageGrab.html) docs). Should also point-out that the file name extension given determines the format of the image file created. – martineau Jun 29 '17 at 16:13
  • Very useful quick and dirty trick, might not suit some cases, but so damn faster to pull off than building a scene object outputing every coordinates and info needed by either PIL or `Tkinter.Canvas` – Nicolas David May 12 '18 at 12:08
  • 2
    `ImportError: ImageGrab is macOS and Windows only` – Martin Thoma Nov 26 '18 at 20:52
  • @MartinThoma Perhaps this article will help: https://stackoverflow.com/questions/69645/take-a-screenshot-via-a-python-script-linux Hopefully you can choose a tool, and crop the image as required. – B.Jenkins Nov 28 '18 at 14:32
  • you could update this to include these imports: `try: from PIL ImageGrab # For Windows & OSx except: import pyscreenshot as ImageGrab # For Linux` – nmz787 Mar 06 '20 at 11:23
  • As of Pillow 7.1.0 ImageGrab supports Linux https://pillow.readthedocs.io/en/stable/releasenotes/7.1.0.html#x11-imagegrab-grab – Olivier Wilkinson Sep 05 '22 at 10:10
9

Use Pillow to convert from Postscript to PNG

from PIL import Image

def save_as_png(canvas,fileName):
    # save postscipt image 
    canvas.postscript(file = fileName + '.eps') 
    # use PIL to convert to PNG 
    img = Image.open(fileName + '.eps') 
    img.save(fileName + '.png', 'png') 
Andy Richter
  • 125
  • 1
  • 3
5

Maybe you can try to use widget_winfo_id to get the HWND of the canvas.

import win32gui

from PIL import ImageGrab

HWND = canvas.winfo_id()  # get the handle of the canvas

rect = win32gui.GetWindowRect(HWND)  # get the coordinate of the canvas

im = ImageGrab.grab(rect)  # get image of the current location
Zoe
  • 27,060
  • 21
  • 118
  • 148
Vincent Zhang
  • 51
  • 1
  • 2
  • 2
    You don't need to use `win32gui` to get the coordinates of the canvas. There are built in `tkinter` methods for that. – TheLizzard Aug 07 '21 at 21:30
1

A better way for @B.Jenkins's answer that doesn't need a reference to the root object:

from PIL import ImageGrab


def save_widget_as_image(widget, file_name):
    ImageGrab.grab(bbox=(
        widget.winfo_rootx(),
        widget.winfo_rooty(),
        widget.winfo_rootx() + widget.winfo_width(),
        widget.winfo_rooty() + widget.winfo_height()
    )).save(file_name)
Alon
  • 36
  • 2
0

On my system had serious issues with ghostscript and the ImageGrab in general. Solution draw on PIL Image, save as a file, load file on PhotoImage, which is used to create new TKinter Canvas.

canvas = Canvas(win, width=IMG_W, height=IMG_H)
img = PILImg.new("RGB", (IMG_W, IMG_H), "#000")
draw = ImageDraw.Draw(img)
draw.rectangle([x,y,w,h], fill=color, outline=border)
img.save("stock-chart.png")
copyImg = PhotoImage(file="stock-chart.png")
canvas.create_image(IMG_W_2, IMG_H_2, image=copyImg)
0

Rather than rely on:

  • hokey, platform-specific screen-capture, or
  • writing images needlessly to disk and then reading them back, or
  • wasting time and memory writing to two canvases

I think it is better to render the canvas to EPS and wrap the PostScript in a BytesIO and allow PIL to read that without using disk:

#!/usr/bin/env python3

from tkinter import *
from PIL import Image, ImageTk
from io import BytesIO
root = Tk()
cv = Canvas(root)
cv.create_rectangle(10,10,50,50, fill='green')
cv.pack()

cv.update()
# Get the EPS corresponding to the canvas
eps = cv.postscript(colormode='color')

# Save canvas as "in-memory" EPS and pass to PIL without writing to disk
im = Image.open(BytesIO(bytes(eps,'ascii')))
im.save('result.png')

root.mainloop()

enter image description here

Mark Setchell
  • 191,897
  • 31
  • 273
  • 432