CustomTkinter icon indicating copy to clipboard operation
CustomTkinter copied to clipboard

Zoom into Image / Label

Open medhanshrath-t opened this issue 1 year ago • 6 comments

It is currently possible to zoom into a label using the mouse scroll? If not, does anyone have an idea how I could implement it?

medhanshrath-t avatar Dec 13 '23 08:12 medhanshrath-t

Hi @medhanshrath-t , do you want to zoom in an image contained in the label? Or just zoom the label text?

LorenzoMattia avatar Dec 13 '23 14:12 LorenzoMattia

I want to zoom into the image, similar to how you can open an image in Windows and then zoom into it.

medhanshrath-t avatar Dec 19 '23 08:12 medhanshrath-t

Hi @medhanshrath-t, you can think about something like this:

import os
import customtkinter
from PIL import Image

class CustomLabel(customtkinter.CTkLabel):
    def __init__(self, master: customtkinter.CTkBaseClass, path_to_image: str, **kwargs) -> None:
        super().__init__(master, **kwargs)

        self.path_to_image = path_to_image
        
        # bind left mouse click to view image method
        self.bind("<Button-1>", self.view_image)

        self.bind("<Enter>", self.on_enter)
        self.bind("<Leave>", self.on_leave)

    def view_image(self, _ = None) -> None:
        # open image with your default os image viewer
        os.system(self.path_to_image)

    # make the label appear as clickable
    def on_enter(self, _ = None) -> None:
        self.configure(cursor = "hand2")

    def on_leave(self, _ = None) -> None:
        self.configure(cursor = "")
        
class App(customtkinter.CTk):
    def __init__(self):
        super().__init__()

        self.geometry("1000x1000")
        
        # create a container frame
        self.main_frame = customtkinter.CTkFrame(self)
        self.main_frame.pack(expand = customtkinter.YES, fill = customtkinter.BOTH)

        # here goes the path to the image you want to show
        image_path = "example.jpg"

        # create the image
        self.img = customtkinter.CTkImage(Image.open(image_path), size = (400, 300))

        self.image_lbl = CustomLabel(self.main_frame, path_to_image=image_path, text="", image = self.img)
        self.image_lbl.pack(expand = customtkinter.YES, fill = customtkinter.BOTH)
        
app = App()
app.mainloop()

The use of the CustomLabel class is not strictly necessary as long as you still bind the event to the corresponding handler method to which you should pass the image path. Even less necessary are the two on_enter and on_leave methods.

Hope this helps!

LorenzoMattia avatar Dec 20 '23 21:12 LorenzoMattia

Hey, this wasn't exactly what I was looking for, I want the zooming to happen in the application, but yeah this is actually useful. I am implementing this as an alternative

medhanshrath-t avatar Dec 21 '23 13:12 medhanshrath-t

Hi, @medhanshrath-t, I am glad that even if not the desired one, my solution is still appreciable for you.

However, if you want to make the label zoom inside the app, you can implement a zooming algorithm using some image processing libraries (PIL, opencv...) and then bind the zoom level to the mouse wheel when hovering over the label, like: self.image_lbl.bind("<MouseWheel>", zoom_function)

LorenzoMattia avatar Dec 23 '23 17:12 LorenzoMattia

I actually found code for a zoomable label for TKinter and modified it so that it works with CTk. It works great on its own, but when I place it in my app it doesn't let me zoom into the sides and once it is zoomed you can not scroll to the edges of the image. I am using grid for packing it.

    """
    Label in which the images can be zoomed into.
    """
    def __init__(self, master=None, **kwargs):
        super().__init__(master, **kwargs)
        self.pil_image = None
        self.zoom_cycle = 0
        self.__old_event = None
        self.width = kwargs['width']
        self.height = kwargs['height']
        self.create_bindings()
        self.reset_transform()

    def create_bindings(self):
        self.master.bind("<Button-1>", self.mouse_down_left)                   # MouseDown
        self.master.bind("<B1-Motion>", self.mouse_move_left)                  # MouseDrag
        self.master.bind("<Double-Button-1>", self.mouse_double_click_left)    # MouseDoubleClick
        self.master.bind("<MouseWheel>", self.mouse_wheel)                     # MouseWheel

    def set_image(self, filename=None, pil_image=None):
        self.pil_image = pil_image if pil_image else Image.open(filename)
        self.zoom_fit(self.pil_image.width, self.pil_image.height)
        self.draw_image(self.pil_image)

    # -------------------------------------------------------------------------------
    # Mouse events
    # -------------------------------------------------------------------------------
    def mouse_down_left(self, event):
        self.__old_event = event

    def mouse_move_left(self, event):
        if (self.pil_image == None):
            return
        
        self.translate(event.x - self.__old_event.x, event.y - self.__old_event.y) if self.__old_event else None
        self.redraw_image()
        self.__old_event = event

    def mouse_double_click_left(self, event):
        if self.pil_image == None:
            return
        self.zoom_fit(self.pil_image.width, self.pil_image.height)
        self.redraw_image() 

    def mouse_wheel(self, event):
        if self.pil_image == None:
            return

        if (event.delta < 0):
            if self.zoom_cycle <= 0:
                return
            # Rotate upwards and shrink
            self.scale_at(0.8, event.x, event.y)
            self.zoom_cycle -= 1
        else:
            if self.zoom_cycle >= 9:
                return
            #  Rotate downwards and enlarge
            self.scale_at(1.25, event.x, event.y)
            self.zoom_cycle += 1
    
        self.redraw_image()

    # -------------------------------------------------------------------------------
    # Affine Transformation for Image Display
    # -------------------------------------------------------------------------------

    def reset_transform(self):
        self.mat_affine = np.eye(3)

    def translate(self, offset_x, offset_y,zoom = False):
        mat = np.eye(3)
        mat[0, 2] = float(offset_x)
        mat[1, 2] = float(offset_y)
        
        scale = self.mat_affine[0, 0]
        max_y = scale * 3072
        max_x = scale * 4096
        self.mat_affine = np.dot(mat, self.mat_affine)

        if not zoom:
            if abs(self.mat_affine[0,2]) > abs(max_x-self.width):
                self.mat_affine[0,2] = -(max_x-self.width)
            if abs(self.mat_affine[1,2]) > abs(max_y-self.height):
                self.mat_affine[1,2] = -(max_y-self.height)

        if self.mat_affine[0, 2] > 0.0:
            self.mat_affine[0, 2] = 0.0
        if self.mat_affine[1,2] > 0.0:
            self.mat_affine[1,2]  = 0.0

    def scale(self, scale:float):
        mat = np.eye(3)
        mat[0, 0] = scale
        mat[1, 1] = scale
        self.mat_affine = np.dot(mat, self.mat_affine)

    def scale_at(self, scale:float, cx:float, cy:float):
        self.translate(-cx, -cy, True)
        self.scale(scale)
        self.translate(cx, cy)

    def zoom_fit(self, image_width, image_height):
        self.master.update()

        if (image_width * image_height <= 0) or (self.width * self.height <= 0):
            return

        self.reset_transform()

        scale = 1.0
        offsetx = 0.0
        offsety = 0.0
        if (self.width * image_height) > (image_width * self.height):
            scale = self.height / image_height
            offsetx = (self.width - image_width * scale) / 2
        else:
            scale = self.width / image_width
            offsety = (self.height - image_height * scale) / 2
        self.scale(scale)
        self.translate(offsetx, offsety)
        self.zoom_cycle = 0

    def to_image_point(self, x, y):
        '''Convert coordinates from the canvas to the image'''
        if self.pil_image == None:
            return []
        mat_inv = np.linalg.inv(self.mat_affine)
        image_point = np.dot(mat_inv, (x, y, 1.))
        if  image_point[0] < 0 or image_point[1] < 0 or image_point[0] > self.pil_image.width or image_point[1] > self.pil_image.height:
            return []
        return image_point
    
    # -------------------------------------------------------------------------------
    # Drawing 
    # -------------------------------------------------------------------------------

    def draw_image(self, pil_image):
        if pil_image == None:
            return

        self.pil_image = pil_image

        mat_inv = np.linalg.inv(self.mat_affine)

        affine_inv = (
            mat_inv[0, 0], mat_inv[0, 1], mat_inv[0, 2],
            mat_inv[1, 0], mat_inv[1, 1], mat_inv[1, 2]
        )

        dst = self.pil_image.transform(
            (self.width, self.height),
            Image.AFFINE,
            affine_inv,
            Image.NEAREST
        )

        ctk_image = CTkImage(light_image=dst, size=(self.width, self.height))
        self.configure(image=ctk_image)

    def redraw_image(self):
        '''Redraw the image'''
        if self.pil_image == None:
            return
        self.draw_image(self.pil_image)```

medhanshrath-t avatar Jan 03 '24 15:01 medhanshrath-t