2

So I followed this post: How can I convert canvas content to an image?

And when I tried to do as mentioned in the last suggestion, I get the following problem:

When I call it out like this, the image/screenshot is taken too early and therefore the required image won't be captured. Here's the code:

from tkinter import *
from PIL import ImageGrab
root = Tk()
cv = Canvas(root)

cv.pack()

cv.create_rectangle(10,10,50,50)
#cv.create_line([0, 10, 10, 10], fill='green')


cv.update()


#print(root.winfo_width())

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

getter(cv)
root.mainloop()

By the way, if there is a simplier solution, I would appreciate it! The thing is that the saving part will be added to the code dynamically later so the solution should be as light as possible.

Thanks in advance!

PS: Maybe it is even possible to save the canvas without displaying it beforehand? Just by the code?

Community
  • 1
  • 1
E. Muuli
  • 3,940
  • 5
  • 22
  • 30
  • 1
    use `root.after(miliseconds, callback)` to execute with delay. O use `PIL` instead of `Tkinter` to draw it. `PIL` has many functions to draw objects. – furas Jan 30 '17 at 16:48
  • Which method I should call out with the delay? root.after(3000, root.mainloop())? – E. Muuli Jan 30 '17 at 17:06
  • 1
    `root.after(3000, getter)` but `callback` means "function name without `()`" - or you have to use `lambda` to add arguments `root.after(3000, lambda:getter(cv))` – furas Jan 30 '17 at 17:08
  • 1
    Or just add the arguments to the root.after call: `root.after(3000, getter, cv). I suspect that `root.update()` before or within the getter call (instead of root.after and mainloop) would also solve your problem for this particular code. – Terry Jan Reedy Jan 30 '17 at 17:38

2 Answers2

4

Below is the code for taking screenshots of just the tkinter canvas. PIL.ImageGrab module does not work in Linux; replaced that with pyscreenshot. Tested this code in Ubuntu 16.04. You may have to check if it operates in Windows/OSx. Please note the remarks in function self._grabtofile.

Remark: In Ubuntu, this script had to be executed directly on commandline/terminal to work. It did not work when executed from IDLE for python3.

Summary:

  1. Able to display the screenschoot of a tkinter canvas and save it to file a using two events.
  2. Able to screenshot tkinter canvas (w/o displaying it) and save it to file using one event.

Working code:

#!/usr/bin/python3
# -*- coding: utf-8 -*-

try:
    import tkinter as tk # Python 3 tkinter modules
except ImportError:
    import Tkinter as tk # Python 2 tkinter modules

from PIL import Image, ImageTk 
#from PIL import Image, ImageTk, ImageGrab  # For Windows & OSx
import pyscreenshot as ImageGrab # For Linux

class App(tk.Frame):
    def __init__(self, parent):
        tk.Frame.__init__(self, parent)
        self.parent=parent

        file = 'images.jpg'
        self.img = Image.open(file)
        #self.img.show() #Check to proof image can be read in and displayed correctly.
        self.photo = ImageTk.PhotoImage(self.img)
        print('size of self.img =', self.img.size)
        centerx= self.img.size[0]//2
        centery= self.img.size[1]//2
        print ('center of self.img = ', centerx, centery)

        self.cv = tk.Canvas(self)
        self.cv.create_image(centerx, centery, image=self.photo)
        self.cv.create_rectangle(centerx*0.5,centery*0.5,centerx*1.5,centery*1.5,
                                 outline='blue')
        self.cv.grid(row=0, column=0, columnspan=3, sticky='nsew') 

        self.snappic=tk.Button(self, text='SNAP', command=self._snapCanvas)
        self.snappic.grid(row=1, column=0, sticky='nsew')

        self.savepic=tk.Button(self, text='SAVE', command=self._save)
        self.savepic.grid(row=1, column=1, sticky='nsew')

        self.directsavepic=tk.Button(self, text='Grab_to_File', command=self._grabtofile)
        self.directsavepic.grid(row=1, column=2, sticky='nsew')

        self.snapsave=tk.Button(self, text='SNAP & SAVE', command=self._snapsaveCanvas)
        self.snapsave.grid(row=2, column=0, columnspan=2, sticky='nsew')

    def _snapCanvas(self):
        print('\n def _snapCanvas(self):')
        canvas = self._canvas() # Get Window Coordinates of Canvas
        self.grabcanvas = ImageGrab.grab(bbox=canvas)
        self.grabcanvas.show()

    def _save(self):
        self.grabcanvas.save("out.jpg")
        print('Screenshoot of tkinter.Canvas saved in "out.jpg"')

    def _grabtofile(self):
        '''Remark: The intension was to directly save a screenshoot of the canvas in
                   "out_grabtofile.png".
                   Issue 1: Only a full screenshot was save.
                   Issue 2: Saved image format defaults to .png. Other format gave errors. 
                   Issue 3: "ImageGrab.grab_to_file" only able to return full screenshoot
                            and not just the canvas. '''
        print('\n def _grabtofile(self):')
        canvas = self._canvas()  # Get Window Coordinates of Canvas
        print('canvas = ', canvas)
        ImageGrab.grab_to_file("out_grabtofile.png", ImageGrab.grab(bbox=canvas))
        print('Screenshoot of tkinter.Canvas directly saved in "out_grabtofile.png"')

    def _snapsaveCanvas(self):
        print('\n def _snapsaveCanvas(self):')
        canvas = self._canvas()  # Get Window Coordinates of Canvas
        self.grabcanvas = ImageGrab.grab(bbox=canvas).save("out_snapsave.jpg")
        print('Screencshot tkinter canvas and saved as "out_snapsave.jpg w/o displaying screenshoot."')

    def _canvas(self):
        print('  def _canvas(self):')
        print('self.cv.winfo_rootx() = ', self.cv.winfo_rootx())
        print('self.cv.winfo_rooty() = ', self.cv.winfo_rooty())
        print('self.cv.winfo_x() =', self.cv.winfo_x())
        print('self.cv.winfo_y() =', self.cv.winfo_y())
        print('self.cv.winfo_width() =', self.cv.winfo_width())
        print('self.cv.winfo_height() =', self.cv.winfo_height())
        x=self.cv.winfo_rootx()+self.cv.winfo_x()
        y=self.cv.winfo_rooty()+self.cv.winfo_y()
        x1=x+self.cv.winfo_width()
        y1=y+self.cv.winfo_height()
        box=(x,y,x1,y1)
        print('box = ', box)
        return box


if __name__ == '__main__':
    root = tk.Tk()
    root.title('App'), root.geometry('300x300')
    app = App(root)
    app.grid(row=0, column=0, sticky='nsew')

    root.rowconfigure(0, weight=1)
    root.columnconfigure(0, weight=1)

    app.rowconfigure(0, weight=10)
    app.rowconfigure(1, weight=1)
    app.columnconfigure(0, weight=1)
    app.columnconfigure(1, weight=1)
    app.columnconfigure(2, weight=1)

    app.mainloop()

Screenshot of GUI:GUI by tkinter

Screenshot of GUI's tk.Canvas: tk.Canvas

Sun Bear
  • 7,594
  • 11
  • 56
  • 102
  • Thanks! Could you also please give me an example on how to use it? For example with my tkinter code. – E. Muuli Feb 01 '17 at 17:52
  • @EerikMuuli Pardon me, I don't understand your question. Please elaborate. If you are asking how to execute this code, then you can save this file with filename "test.py", open a terminal and type python3 test.py to execute it. As is, if you study the code, it shows you how to apply what you wrote in your code (with certain corrections or amendments) how to screenshot your GUI tkinter canvas. I have added a lot of print statements and comments to help you study the code. – Sun Bear Feb 01 '17 at 19:21
  • Sorry for not elaborating on it. I got it to work - it opens up a screen with buttons. But is it possible to save Canvas into image file automatically. For example if I have a code (that draws a canvas) and I want to automatically save it into an image. So that for example if I add the "canvas drawing code" into your code then executing test.py from command line would save the output of the canvas into a file. (No user input whatsoever in the process). I hope that makes it more clear. – E. Muuli Feb 01 '17 at 19:33
  • Could you explain what you mean by "automatically"? As is, the code has already generate a GUI with a tk.Canvas and buttons for you to take screenshots of the tk.Canvas (and even the monitor screen) whenever you need; you just have to click on them. – Sun Bear Feb 01 '17 at 19:47
  • @EerikMuuli You have two options. Transfer your drawing code into my present code or the other way round is to transfer my code in to your drawing code. In the first option, when you have run the code, a GUI will appear to allow you to draw on the canvas. Once done, click on the "Snap & Save" and a screenshot of the canvas will automatically be saved into your working directory. I can't comment on the 2nd option as I do not know your code. I have added 2 screenshots. One shows the GUI and another the canvas with an image of Tux and a box created by `tk.Canvas.create_rectangle`. – Sun Bear Feb 01 '17 at 20:12
  • By automatically I mean that no user input is required at all. No button pressing etc. You just execute the code and the screenshot will be taken. Is that possible? – E. Muuli Feb 01 '17 at 20:15
  • @EerikMuuli Sure you can. Just use the relevant section of the working codes to screen shot the canvas.The codes in `def _snapsaveCanvas(self)` and `def _canvas(self)` are what you will mainly need. – Sun Bear Feb 01 '17 at 20:32
  • Okay, thanks! But currently it requires image as an input, but how to convert it so that it would take for example widget as an input? Or I can already use widget as the "self" input? – E. Muuli Feb 01 '17 at 20:52
  • No need to convert anything. Just remove the command self.cv.create_image(centerx, centery, image=self.photo). You have to read more about creating graphics, text, widgets, or frames on the canvas [here](http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/canvas.html). We have to avoid extended discussions in comments so let stop here. Appreciate an up vote if my answer is useful (or even click answered if I have answered your question). – Sun Bear Feb 01 '17 at 21:27
  • Okay, here's what I've got so far: http://pastebin.com/0ptpqHx5 It is working perfectly fine in my Win PC from idle and also from command line. But in the Linux it does not (at least from command live where I need it to work), I get the following error: https://snag.gy/6KdRPx.jpg, https://snag.gy/LHEZih.jpg. I think I've got everything right on this one, but it still doesn't run on the Linux machine. Do you have any ideas? – E. Muuli Feb 02 '17 at 15:42
  • 1
    @EerikMuuli You need to add `from PIL import Image` in line 3 of your code. The save command in line 20 is from PIL.Image. Another thing, I noticed in line 9 of your code you used `cv.update()`. Is there a reason why you need it? Please read [here](http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/universal.html). I think `cv.update_idletasks()` is more appropriate. – Sun Bear Feb 02 '17 at 16:32
2

I know this is an old post but I just want to share what worked for me, hopefully it will help somebody with similar problem.

import tkinter as tk
from PIL import Image
import io
import os
import subprocess

root = Tk()

cv = Canvas(root)
cv.pack()

cv.create_rectangle(10,10,50,50)

ps = cv.postscript(colormode='color')
img = Image.open(io.BytesIO(ps.encode('utf-8')))
img.save('filename.jpg', 'jpeg')

root.mainloop()

In this solution, you have to have ghostscript and Pillow installed. For MacOS, install ghostscript with

brew install ghostscript

and Pillow with pip install Pillow

syncster31
  • 66
  • 2
  • 1
    I am getting the error " raise WindowsError('Unable to locate Ghostscript on paths')" due to the img.save line. Any ideas? – Relative0 May 21 '19 at 23:20