Tkinter Pillow Application 0.4

This is Version 0.4 of my Tkinter Pillow Application which adds the following functionality:

  • A dialog box prompting the user to select image quality on saving

  • A prompt to save any changes when an image or the application are closed

  • Buttons in the Resize dialog to calculate the height or width from the other value to maintain the height/width ratio

The first three versions were:

Version 0.1

Version 0.2

Version 0.3

This is a screenshot of Version 0.4.

The Code

The project currently consists of four Python files:

  • pillowappengine.py

  • pillowapptkinter.py

  • resizedialog.py

  • qualitydialog.py

as well as a few graphics files for the buttons and icon. The files can be downloaded as a zip, or you can clone/download the Github repository if you prefer. In this post I will only show the code which has been added or changed since Version 0.3.

Source Code Links

ZIP File
GitHub

Quality Dialog

I have created a new QualityDialog class in qualitydialog.py which contains a Scale control (the is the Tkinter terminology for what is generally known as a slider). This is displayed by the application when the user selects Save or Save As, and the selected quality is passed to the PillowAppEngine's save and save_as methods.

According to the Pillow documentation https://pillow.readthedocs.io/en/5.1.x/handbook/image-file-formats.html

The image quality, on a scale from 1 (worst) to 95 (best). The default is 75. Values above 95 should be avoided; 100 disables portions of the JPEG compression algorithm, and results in large files with hardly any gain in image quality.

I have therefore set the Scale control to allow values between 1 and 95 with a default of 75. Although you can go all the way down to 1 you probably wouldn't want to. This is why...

Not nice is it?!

This is the QualityDialog class.

qualitydialog.py

from tkinter import *


class QualityDialog(object):

    def __init__(self, quality):

        self.quality = quality

        self.dlg = Toplevel()

        self.dlg.title("Quality")

        self.dlg.grab_set()

        self.slider = Scale(self.dlg, from_=1, to=95, length=200, orient=HORIZONTAL)
        self.slider.set(quality)
        self.slider.grid(row=0, column=0, padx=4, pady=4, sticky=W)

        self.ok_button = Button(self.dlg, text='OK', command=self.ok)
        self.ok_button.grid(row=2, column=0, columnspan=3, padx=4, pady=4, sticky=W+E)


    def ok(self):

        self.quality = self.slider.get()

        self.dlg.destroy()

It uses the same principle as the ResizeDialog class introduced in Version 0.3 but uses a Scale control. The OK button click event handler then assigns the user-selected value to the quality attribute for use by calling code.

These are the save and save_as functions in pillowapptkinter.py, edited to use the new dialog. The corresponding methods in the PillowAppEngine have quality arguments added, which are used in the Pillow Image class's save method.

pillowapptkinter.py

    def save(self):

        """
        Used as an event handler for menu and toolbar button.
        """

        if self.pae.image is not None:
            try:
                quality = 75
                quality_dialog = qualitydialog.QualityDialog(quality)
                self.window.wait_window(quality_dialog.dlg)
                self.pae.save(quality_dialog.quality)
            except Exception as e:
                self.show_error_message("Save", e)


    def save_as(self):

        """
        Used as an event handler for menu.
        """

        try:
            filepath = filedialog.asksaveasfile(title="Save image as", filetypes=(("JPEG files", "*.jpg"),))
            # Clicking Cancel returns None.
            if filepath is not None:
                quality = 75
                quality_dialog = qualitydialog.QualityDialog(quality)
                self.window.wait_window(quality_dialog.dlg)
                self.pae.save_as(filepath.name, quality_dialog.quality)
        except Exception as e:
            self.show_error_message("Save as", e)

Prompt to Save Changes

This is pretty simple to implement: in on_quit (called when the application is closed) and close (which closes the current image but not the application) we call the prompt_to_save function.

pillowapptkinter.py

    def on_quit(self):

        """
        Checks for any unsaved changes, then closes the application.
        """

        self.prompt_to_save()

        self.window.destroy()
.
.
.
    def close(self):

        """
        Used as an event handler for menu.
        """

        self.prompt_to_save()

        self.pae.close()
        self.image_canvas.grid_forget()
.
.
.
    def prompt_to_save(self):

        if self.pae.saved == False:
            if messagebox.askyesno("Save changes", "Do you want to save changes?") == True:
                if self.pae.filepath is not None:
                    self.save()
                else:
                    self.save_as()

Calculating Height and Width in Resize Dialog

For this I have added a couple of buttons to the ResizeDialog class along with the corrresponding click event handlers.

This is the entire class with new code in yellow. Note that I have also added validation.

resizedialog.py

from tkinter import *


class ResizeDialog(object):

    def __init__(self, current_dimensions, new_dimensions):

        self.new_dimensions = new_dimensions
        self.current_dimensions = current_dimensions

        self.dlg = Toplevel()

        self.dlg.title("Resize")

        self.dlg.grab_set()

        self.width_label = Label(self.dlg, text="Width")
        self.width_label.grid(row=0, column=0, padx=4, pady=4, sticky=W)

        self.width_box = Entry(self.dlg)
        self.width_box.insert(0, current_dimensions["width"])
        self.width_box.grid(row=0, column=1, padx=4, pady=4, sticky=W)

        self.calculate_width_button = Button(self.dlg, text='Calculate', command=self.calculate_width)
        self.calculate_width_button.grid(row=0, column=2, padx=4, pady=4, sticky=W)

        self.height_label = Label(self.dlg, text="Height")
        self.height_label.grid(row=1, column=0, padx=4, pady=4, sticky=W)

        self.height_box = Entry(self.dlg)
        self.height_box.insert(0, current_dimensions["height"])
        self.height_box.grid(row=1, column=1, padx=4, pady=4, sticky=W)

        self.calculate_height_button = Button(self.dlg, text='Calculate', command=self.calculate_height)
        self.calculate_height_button.grid(row=1, column=2, padx=4, pady=4, sticky=W)

        self.ok_button = Button(self.dlg, text='OK', command=self.ok)
        self.ok_button.grid(row=2, column=0, columnspan=3, padx=4, pady=4, sticky=W+E)

        self.cancel_button = Button(self.dlg, text='Cancel', command=self.cancel)
        self.cancel_button.grid(row=3, column=0, columnspan=3, padx=4, pady=4, sticky=W+E)


    def ok(self):

        width_valid = self.get_width()

        height_valid = self.get_height()

        if width_valid and height_valid:
            self.dlg.destroy()


    def cancel(self):

        self.new_dimensions["width"] = 0
        self.new_dimensions["height"] = 0

        self.dlg.destroy()

    def get_width(self):

        try:
            self.width_box.config(bg="white")
            self.new_dimensions["width"] = int(self.width_box.get())
            if self.new_dimensions["width"] <= 0:
                self.width_box.config(bg="yellow")
                return False
            else:
                return True
        except ValueError:
            self.width_box.config(bg="yellow")
            return False

    def get_height(self):

        try:
            self.height_box.config(bg="white")
            self.new_dimensions["height"] = int(self.height_box.get())
            if self.new_dimensions["height"] <= 0:
                self.height_box.config(bg="yellow")
                return False
            else:
                return True
        except ValueError:
            self.height_box.config(bg="yellow")
            return False

    def calculate_height(self):

        width_valid = self.get_width()

        if width_valid:
            h_w_ratio = self.current_dimensions["width"] / self.current_dimensions["height"]
            self.new_dimensions["height"] = int(self.new_dimensions["width"] / h_w_ratio)
            self.height_box.delete(0, "end")
            self.height_box.insert(0, self.new_dimensions["height"])


    def calculate_width(self):

        height_valid = self.get_height()

        if height_valid:
            h_w_ratio = self.current_dimensions["width"] / self.current_dimensions["height"]
            self.new_dimensions["width"] = int(self.new_dimensions["height"] * h_w_ratio)
            self.width_box.delete(0, "end")
            self.width_box.insert(0, self.new_dimensions["width"])

The current image dimensions are passed to the dialog and used as default values for the Entry controls, as well as being used to calculate the height/width ratio of the image.

This is the dialog.

Let's run the program and try out the new functionality.

Running the program

python3.7 pillowapptkinter.py