Search code examples
pythonimagetkinterpython-imaging-libraryimage-resizing

Dynamic resizing of image using PIL and tkinter


I wanted to know if it was possible to resize the image dynamically(maintaining its aspect ratio). I made an image viewer app, but then the vertically long images overflow the screen, so i wanted to know a method to resize image, ive tried a way and its included below. But still im getting the same output that overflows the screen.

from win32api import GetSystemMetrics
from tkinter import *

screen_width, screen_height = GetSystemMetrics(0), GetSystemMetrics(1)

root = Tk() # this is your window
root.geometry("{}x{}".format(screen_width//2, screen_height//2)) # set size of you window here is example for 1/2 screen height and width

img = Image.open("picture_name.png")
width, height = screen_width//4, screen_height//4 

img.resize((width, height), Image.ANTIALIAS) 

l = Label(root,image=img)
l.pack()

root.mainloop()

Still im getting an image that is not resized, dont know why.

Then i tried out this method, where I set a resolution, and it works fine for my screen. but if i were to send to someother people, it would not dynamically adjust.

desired_size = 950

im = Image.open('img.png')
old_size = im.size

ratio = float(desired_size)/max(old_size)
new_size = tuple([int(x*ratio) for x in old_size])

im = im.resize(new_size, Image.ANTIALIAS)
img = ImageTk.PhotoImage(im)
l = Label(root, image=img)
l.image = img
l.pack()

I would like to know a way to dynamically resize the image maintaining its aspect ratio as well, so no distortion takes place, like the Photos app in Windows 10.

Whole code:

from tkinter import *
from tkinter import messagebox
from glob import glob
from tkinter import filedialog
from PIL import Image, ImageTk

root = Tk()
root.config(bg='white')
root.title('Image Viewer App')

def forward_image(event=None):
    global n
    n += 1
    if n > len(main_img)-2:
        forward['state'] = DISABLED
        root.unbind('<Key-Right>')

    else:
        backward['state'] = NORMAL
        root.bind('<Key-Left>', backward_image)

    im = Image.open(main_img[n])
    old_size = im.size

    ratio = float(desired_size)/max(old_size)
    new_size = tuple([int(x*ratio) for x in old_size])

    im = im.resize(new_size, Image.ANTIALIAS)
    img = ImageTk.PhotoImage(im)
    l.image = img
    l.config(image=img)
    status.config(text=f'{n+1} of {total} images')

def backward_image(event=None):
    global n
    n -= 1
    if n <= 0:
        backward['state'] = DISABLED
        root.unbind('<Key-Left>')

    else:
        forward['state'] = NORMAL
        root.bind('<Key-Right>', forward_image)

    im = Image.open(main_img[n])
    old_size = im.size

    ratio = float(desired_size)/max(old_size)
    new_size = tuple([int(x*ratio) for x in old_size])

    im = im.resize(new_size, Image.ANTIALIAS)
    img = ImageTk.PhotoImage(im)
    l.image = img
    l.config(image=img)
    status.config(text=f'{n+1} of {total} images')

def path():
    global main_img
    path = filedialog.askdirectory(
        initialdir='c:/', title='Select a folder with images')
    img_png = glob(path+'/*.png')
    img_jpg = glob(path+'/*.jpg')
    main_img = img_jpg + img_png


path()

n = 0
desired_size = 950

im = Image.open(main_img[n])
old_size = im.size

ratio = float(desired_size)/max(old_size)
new_size = tuple([int(x*ratio) for x in old_size])

im = im.resize(new_size, Image.ANTIALIAS)
img = ImageTk.PhotoImage(im)
l = Label(root, image=img)
l.image = img
l.pack()


forward = Button(root, text='Forward', command=forward_image)
forward.pack(side=RIGHT)

backward = Button(root, text='Backward', command=backward_image)
backward.pack(side=LEFT)
backward['state'] = DISABLED

total = len(main_img)
status = Label(root,text=f'{n+1} of {total} images',bg='white',font=('helvetica',10))
status.pack(side=BOTTOM)

root.focus_force()

root.bind('<Key-Left>', backward_image)
root.bind('<Key-Right>', forward_image)
root.bind('<Escape>', lambda event: root.state('normal'))
root.bind('<F11>', lambda event: root.state('zoomed'))

if total <= 1:
    backward['state'] = DISABLED
    forward['state'] = DISABLED
    root.unbind('<Key-Right>')
    root.unbind('<Key-Left>')

if total == 0:
    messagebox.showerror('No image','Choose a directory with images.')
root.mainloop()

Thanks in advance :D


Solution

  • Here ya' go. With a scale of 1.0 or lower the image will always fit in it's master. This answer is based on @HenryYik answer, but made more dynamic through the addition of the scale argument, and the logic to consider overflow in every direction. Also, instead of being based on window screenspace it's based on master screenspace, and that consideration is made in resizing, as opposed to in __init__.

    other changes:

    • Using super() to __init__ a superclass is not ideal, so that part has been changed to a more strict syntax.
    • Unless you have a running list in your head of the exact order of all kwargs, for every widget, you will never use *args, so it has been omitted.

    import tkinter as tk
    from tkinter import messagebox, filedialog
    from glob import glob
    from PIL import Image, ImageTk
    
    #configure root
    root = tk.Tk()
    root.title('Image Viewer App')
    root.geometry('800x600')
    root.config(bg='#222222',bd=0,padx=0,pady=0,highlightthickness=0)
    root.bind('<Escape>', lambda event: root.state('normal'))
    root.bind('<F11>', lambda event: root.state('zoomed'))
        
    
    class Slide(tk.Label):
        def __init__(self, master, image_path:str='', scale:float=1.0, **kwargs):
            tk.Label.__init__(self, master, **kwargs)
            self.configure(bg=master['bg'])
            self.img   = None if not image_path else Image.open(image_path)
            self.p_img = None
            self.scale = scale
                    
            self.bind("<Configure>", self.resizing)
            
        def set_image(self, image_path:str):
            self.img   = Image.open(image_path)
            self.resizing()
    
        def resizing(self, event=None):
            if self.img:
                iw, ih  = self.img.width, self.img.height
                mw, mh  = self.master.winfo_width(), self.master.winfo_height()
                
                if iw>ih:
                    ih = ih*(mw/iw)
                    r = mh/ih if (ih/mh) > 1 else 1
                    iw, ih = mw*r, ih*r
                else:
                    iw = iw*(mh/ih)
                    r = mw/iw if (iw/mw) > 1 else 1
                    iw, ih = iw*r, mh*r
                    
                self.p_img = ImageTk.PhotoImage(self.img.resize((int(iw*self.scale), int(ih*self.scale))))
                self.config(image=self.p_img)
    
    
    
    total     = 0
    slide_num = 0
    
    def get_slides():
        global total
        path  = filedialog.askdirectory(initialdir='c:/', title='Select a folder with images')
        cache = glob(path+'/*.png') + glob(path+'/*.jpg')
        
        total = len(cache)
        if not total:
            m = messagebox.askyesno('No Images','The directory you have chosen does not contain any images. Try Again?')
            if m:
                return get_slides()
            else:
                root.quit()
                exit(0)
            
        return cache
    
    
    image_cache = get_slides()
    
    
    def commit_slide(n, t):
        slide.set_image(image_cache[n])
        status.config(text=f'{n+1} of {t} images')
    
        
    def next_slide(event=None):
        global slide_num, total
        slide_num = (slide_num+1)%len(image_cache)       #wrap
        commit_slide(slide_num, total)
        
    root.bind('<Key-Right>', next_slide)
    
    
    def previous_slide(event=None):
        global slide_num, total
        slide_num = range(len(image_cache))[slide_num-1] #wrap
        commit_slide(slide_num, total)
        
    root.bind('<Key-Left>', previous_slide)
    
    
    #init display widgets
    slide = Slide(root)
    slide.pack()
    
    tk.Button(root, text='prev', command=previous_slide).place(relx=.02, rely=.99, anchor='sw')
    tk.Button(root, text='next', command=next_slide).place(relx=.98, rely=.99, anchor='se')
    
    status = tk.Label(root, bg='white', font=('helvetica',10))
    status.place(relx=.5, rely=.99, anchor='s')
    
    #init first slide
    commit_slide(slide_num, total)
    
    root.focus_force()
    root.mainloop()