python – FIXED: How to export/save a Tkinter canvas that has been zoomed in?

I made a Tkinter program where I can load an image, draw rectangles on it, zoom and move the canvas. But when I want to save / export the canvas, the resulting image only saves what is visible inside the widget at the time of the save. Since I will be working on very large images I need to be able to zoom in. Also, if I zoom out before saving, the quality is just going to be awful.

Question: How can I save the whole canvas in good quality (image, shapes, text) even when zoomed in?

Here is an example of what the program looks like when zoomed in.
Program when zoomed in

The result only show what is seen at the moment of the export (and it’s a bit blurry).

Blurry image:

enter image description here

Here is a picture of the de-zoomed program so you can see the whole Image that I would like to get exported (no, I can’t just zoom out and export because it would be too blurry, this image is an example, I will be working or much bigger images).
bigger images:

enter image description here

Here is my code, I explain the functions under the code (shorten version so you can test it, yet still a bit long because of the functions):

from tkinter import*
from tkinter import EventType
from tkinter import filedialog
from PIL import ImageTk, Image
from PIL import EpsImagePlugin    #to save image as eps
EpsImagePlugin.gs_windows_binary =  r'C:Program Filesgsgs9.56.1bingswin64c.exe'  #so you don't have to add GS to the path

def save (even=1):
    canvas.postscript(file = "Result" + '.eps')   # save postscipt image
    img = Image.open("Result" + '.eps')    # use PIL to convert to PNG
    img.load(scale=10)
    img.save('Result.png')  #needs to download ghostscript

def do_zoom(event):
  global imageWidth, imageHeight, imageZoom, schematicImage, imageAffichee, imageTemp, imageXPos, imageYPos
  if (event.delta > 0): #zoom
    imageZoom=imageZoom*1.1
    canvas.scale("all", canvas.canvasx(event.x), canvas.canvasy(event.y), 1.1, 1.1) #scale all shapes
    canvas.scale(imageAffichee, canvas.canvasx(event.x), canvas.canvasy(event.y), 1.1, 1.1)
    imageXPos+=(canvas.coords(imageAffichee)[0]-canvas.canvasx(event.x))/(12+1/9) #reposition the image according to the zoom
    imageYPos+=(canvas.coords(imageAffichee)[1]-canvas.canvasy(event.y))/(12+1/9) #ratio: /12.11111 inf
  elif (event.delta < 0): #dezoom
    imageZoom=imageZoom/1.1
    canvas.scale("all", canvas.canvasx(event.x), canvas.canvasy(event.y), 1/1.1, 1/1.1) #scale all shapes
    canvas.scale(imageAffichee, canvas.canvasx(event.x), canvas.canvasy(event.y), 1/1.1, 1/1.1)
    imageXPos+=(canvas.canvasx(event.x)-canvas.coords(imageAffichee)[0])*0.11   #reposition the image according to the zoom
    imageYPos+=(canvas.canvasy(event.y)-canvas.coords(imageAffichee)[1])*0.11   #ratio 0.11
  newImage = imageTemp. resize((int(float(imageWidth)*imageZoom), int(float(imageHeight)*imageZoom))) #create new image
  schematicImage = ImageTk.PhotoImage(newImage) #turns new image into photoImage
  imageAffichee=canvas.create_image(imageXPos, imageYPos,image=schematicImage) #show new image
  canvas.lower(imageAffichee) #put the image behind the rectangles

def loadImage(event=1):
    global schematicImage, imageAffichee, imageHeight, imageWidth, imageTemp, imageZoom, imageXPos, imageYPos
    imageZoom=1
    imageXPos=0
    imageYPos=0
    root.filename=filedialog.askopenfilename(title="select file")   #get file location
    imageTemp=Image.open(root.filename) #open image
    schematicImage = ImageTk.PhotoImage(imageTemp)  #Turns it into a photoImage
    imageWidth=schematicImage.width()   #save widht for rescaing (and maybe saving)
    imageHeight=schematicImage.height() #save height for rescaing (and maybe saving)
    imageAffichee=canvas.create_image(0,0,image=schematicImage) #print image on the canvas
    canvas.create_rectangle(0,0,100,200)

root=Tk()   #create root
root.title("HELP MEEEEE!")
widthWindow=500
heightWindow=400
root.geometry(f"{widthWindow}x{heightWindow}")
root.update()

imageZoom=1.0   #Variable

#Drop down menu
menu=Menu(root)
root.config(menu=menu)
fileMenu = Menu(menu)   #first menu
menu.add_cascade(label="File",menu=fileMenu)
fileMenu.add_command(label="Load new schematic (a)",command=loadImage)
fileMenu.add_command(label="Export (w)", command = save)
fileMenu.add_separator()    #create a spacer
fileMenu.add_command(label="Exit",command=root.quit)

#Panel creation
imagePanel=PanedWindow(root, bd=1, bg="black", orient=VERTICAL) #left panel "image"
imagePanel.place(relheight=1, relwidth=0.75)
excelPanel=PanedWindow(root, bd=1, bg="black", orient=VERTICAL) #right panel "excel" (it s just text)
excelPanel.place(relx=0.75, relheight=1, relwidth=0.25) #relative position of the right panel
excelPanel.update()
textExcel=Text(excelPanel, undo=True)   #Creation of text widget inside the right panel
textExcel.pack(side=TOP, fill=BOTH, expand=1)

#Canva creation
canvas= Canvas(imagePanel, width=imagePanel.winfo_width(), height=imagePanel.winfo_height(), bg='#333333')    #Creation de canvas
canvas.pack(fill=BOTH, expand=1)

root.bind_all("<MouseWheel>", do_zoom)  #bind mousewheel to the zoom function
canvas.bind('<Control-ButtonPress-1>', lambda event: canvas.scan_mark(event.x, event.y))        #first part of the move

function
canvas.bind("<Control-B1-Motion>", lambda event: canvas.scan_dragto(event.x, event.y, gain=1))  #second part of the move function

root.mainloop() #window's main loop
  • save function: the one I have issue with. I expected postscript to save the whole canvas (unlike ImageGrab.grab) but no.

  • do_zoom function: Scale the rectangle and redraw the image (needs to split those 2 actions)

  • loadImage: allows you to load an image from your file explorer (not pdfs though)

To test my program, make sure you load an image first by clicking on the “File” menu on the top left corner, then try to export it after zooming in by clicking “Export” in the same file menu.

EDIT: I used acw1668’s answer to fix my issue, however, when zoomed out, if I tried to export the canvas, the image would become very blurry. So I added some lines to force a zoom ratio of 1 before exporting. Here is the new and working save function:

def save(event=1):
global imageZoom, imageXPos, imageYPos, imageAffichee, imageWidth, imageHeight, schematicImage, imageTemp
while(imageZoom != 1):  #run as long is the zoom ratio isn't 1
    if (imageZoom<1): #if it s less than 1, we have to zoom in
      imageZoom=imageZoom*1.1 #update new zoom ratio
      canvas.scale("all", canvas.canvasx(0), canvas.canvasy(0), 1.1, 1.1) #Scale all shapes
      canvas.scale(imageAffichee, canvas.canvasx(0), canvas.canvasy(0), 1.1, 1.1) #Scale the image
      imageXPos+=(canvas.coords(imageAffichee)[0]-canvas.canvasx(0))/(12+1/9)   #calculate new x position of the image
      imageYPos+=(canvas.coords(imageAffichee)[1]-canvas.canvasy(0))/(12+1/9)   #calculate new y position of the image
    else: 
      imageZoom=imageZoom/1.1 #same but if zoom is greater than 1
      canvas.scale("all", canvas.canvasx(0), canvas.canvasy(0), 1/1.1, 1/1.1)
      canvas.scale(imageAffichee, canvas.canvasx(0), canvas.canvasy(0), 1/1.1, 1/1.1)
      imageXPos+=(canvas.canvasx(0)-canvas.coords(imageAffichee)[0])*0.11 #C'est bon, ne pas toucher au ratio! (0.11)
      imageYPos+=(canvas.canvasy(0)-canvas.coords(imageAffichee)[1])*0.11
    for child_widget in canvas.find_withtag("text"):  #find all the ites with a tag "text"
      canvas.itemconfigure(child_widget, font = ("Helvetica", 10))  #change the font size to 10
    newImage = imageTemp. resize((int(float(imageWidth)*imageZoom), int(float(imageHeight)*imageZoom))) #create new image
    schematicImage = ImageTk.PhotoImage(newImage) #create new image
    imageAffichee=canvas.create_image(imageXPos, imageYPos,image=schematicImage) #create new image
    canvas.lower(imageAffichee) #put the image behind the rectangles
  # get the coordinates of the bounding box of all canvas items
  x1, y1, x2, y2 = canvas.bbox(imageAffichee)
  # calculate the width and height of the bounding box
  w, h = x2-x1, y2-y1
  canvas.postscript(file="Result.eps", x=x1, y=y1, width=w, height=h, pagewidth=w, pageheight=h)   # save postscript image
  img = Image.open("Result.eps")    # use PIL to convert to PNG
  img.save('Result.png')  #needs to download ghostscript

Leave a Comment