0

Hello I'm working on a little paint program. It basically draws circles on a tkinter Canvas when mouse is pressed and simultaneously draws circle on an PIL image at the same time (with same circle properties).

My problem is that with a too small circle radius, and when I move too fast to draw a chain of circles, I don't have the same tkinter output and PIL image output.

I'm using python 3.8.5 and PIL 7.0.0

#! /usr/bin/env python3

import tkinter as tk
from tkinter import *
from PIL import Image,ImageDraw,ImageTk

class App(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        #Tkinter Classical Canvas:
        self.canv= Canvas(self, bg="white", height=500,
                              width=500)
        self.canv.pack()
        #identic sized  PIL image:
        self.image1 = Image.new("RGB", (500, 500), "white")
        self.draw = ImageDraw.Draw(self.image1)
        self.buttons()

    def buttons(self):
        #size of the diameter parameter "button":
        self.choose_diam_button =Scale(self, from_=1, to=100, orient=HORIZONTAL)
        self.choose_diam_button.pack()

        self.bouton_save = Button(self, text="save",font = "Ubuntu",
                                        command=self.save)
        self.bouton_save.pack()

        self.setup()

    def save(self):
        self.chiffre_status=0
        #the file is saved in an "IMG_DATA" gif file in directory
        self.filename = "IMG_DATA"
        self.image1.save(self.filename , format='GIF')



    def reset(self, event):
        self.old_x, self.old_y = None, None

    def setup(self):
        self.old_x = None
        self.old_y = None
        #boutton de taille du pinceau
        self.diameter = self.choose_diam_button.get()
        self.color = 'black'

        self.canv.bind('<B1-Motion>', self.paint)
        self.canv.bind('<ButtonRelease-1>', self.reset)

    def paint(self,event):
        self.diameter = self.choose_diam_button.get()
        if self.old_x and self.old_y:
          self.x=event.x+self.diameter
          self.y=event.y+self.diameter
          #The next line draws on the visible tkinter Canvas
          self.canv.create_oval(self.old_x, self.old_y,self.x ,self.y ,
                                outline=self.color, fill=self.color)
        #The next line draws on the PIL invisible image1
          self.draw.ellipse([self.old_x, self.old_y, self.x, self.y],outline=self.color, fill=self.color)

        self.old_x = event.x
        self.old_y = event.y

appli = App()
appli.title("stack question")
appli.mainloop()

Tkinter visible output:

enter image description here

Saved "Parallel" PIL image:

enter image description here

  • Check if this answer works: https://stackoverflow.com/questions/9886274/how-can-i-convert-canvas-content-to-an-image – JacksonPro Dec 13 '20 at 15:32

1 Answers1

0

Ok 2 solutions found to my own question, if anyone working on Linux pass by:

Adapted from this post:

How can I convert canvas content to an image?

For anyone willing to save a Canvas in any format by first saving an eps file on the disk: Just change the save function above with the one below: The parallel PIL drawing is now useless, you can remove it. It will first save a postscript file and then re-save it in proper format. Watch out for the postscript size arguments. (adapt to yours)

def save(self):
    # save postscipt image 
    #!!!!!(NOTICE the pagewidth and pageheight arguments)!!!!!
    self.canv.postscript(file = "IMG_DATA" + '.eps',pagewidth=500, pageheight=500)
    # use PIL to convert to GIF
    self.img = Image.open("IMG_DATA" + '.eps')
    self.img.save("IMG_DATA" + '.gif', 'gif')

Edit:

Best solution:

Another solution taken from the link above (following one of the answer below) is to save directly with ImageGrab.grab() giving the coordinates of the images without saving anyfile on disk. On linux there is no ImageGrab,so you'll just have to install pyscreenshot:

import pyscreenshot as ImageGrab


    def save(self):
        #replace self.canv with your widget name
        x=self.winfo_rootx()+self.canv.winfo_x()
        y=self.winfo_rooty()+self.canv.winfo_y()
        x1=x+self.canv.winfo_width()
        y1=y+self.canv.winfo_height()
        ImageGrab.grab().crop((x,y,x1,y1)).save("IMG_DATA.gif")

Edit(2) :

Found a problem with the "best solution": When your "paint" window isn't full screen and the widget you want to copy, a little shifted out of the screen, you may have this problem: The part that is not on screen can't be copied and is replaced by a black rectangle.

enter image description here

  • You can use `ImageGrab.grab(...)` to take a snapshot of the canvas to a image and save the image. – acw1668 Dec 13 '20 at 16:21
  • You don't need to save it to a file on disk. You can save to an *"in-memory"* `BytesIO` and read that with PIL to save waiting for harddisks to rotate and creating unnecessary files in your filesystem. – Mark Setchell Dec 13 '20 at 17:00