Введение

Иногда необходимо изменить размер изображения с сохранением пропорции сторон. Особенно, когда это очень большое количество файлов. Это приложение позволяет изменить размер изображения, сохранить его в нужную папку, а также инвертировать цвет (подходит для редактирования осциллограмм) и переименовать файл. Возможно, для существуют уже какие-то программы, но была необходимость сделать это самому. Поделюсь кодом с вами, возможно кому-то потребуется (Код программы в конце статьи).

Функции программы

Перди мной стояла задача, сделать простую программу позволяющую изменять размер изображения с сохранением пропорции сторон. Долго недумая решил написать небольшой скрипт, который удовлетворял бы такие потребности:

  1. Возможность работы с разными типами файлов изображений (png, bmp, jpg и т.д.)

  2. Удобно добавлять файлы в программу

  3. Возможность добавлять сразу несколько изображений

  4. Возможность переименовывать отредактированный файл

  5. Возможность инвертировать цвета

  6. Удобный выбор пути для сохранения файлов

  7. Простой интерфейс программы

Внешний вид

Сначала рассмотрим, как выглядит программа и что выполняет, а затем перейдем к самой реализации.

Интерфейс программы выглядит следующим образом:

Рис. 1. Внешний вид
Рис. 1. Внешний вид

Как видим, программа не перегружена лишними деталями. Разберем каждый элемент подробнее.

  • Image files - выбор изображений, которые нужно будет редактировать. Откроется стандартный файловый проводник.

  • Save in folder - выбор папки куда будет сохранен редактируемый файл если пользователь не выберет папку, они будут сохранены в папке корня программы под названием Edited photos (папка создается автоматически). Откроется стандартный файловый проводник.

  • Image height (cm) - здесь нужно внести высоту изображения, которая необходима (ширина будет отредактирована автоматически с сохранением пропорций)

  • Inver color - при выборе этого чекбокса, цвета изображения будут инвертированы в RGBA (Это очень удобно если работаете с изображением осциллограмм)

  • Rename the file - при выборе чекбокса разблокирует поле ввода "Add ending to file name" и дописывает в название файла соответствующий текст.
    Пример: название изображения "x". В поле Add ending to file name указываем "edit". Файл или файлы сохраняются с таким названием: "x_edit".
    Если чекбокс не выбран, то файл сохранится с таким же названием ("x")

  • Convert - редактирование файла и сохранение с соответствующими параметрами указанными выше.

Рис.2 Результат выполнения программы
Рис.2 Результат выполнения программы

Реализация программы

Код был написан на Python с использованием следующих библиотек:

from tkinter import *
from tkinter import messagebox
from tkinter import filedialog
import PIL.ImageOps
from PIL import Image
import errno
import os

Полный код программы на Python:

from tkinter import *
from tkinter import messagebox
from tkinter import filedialog
import PIL.ImageOps
from PIL import Image
import errno
import os

##--------------------------------------------------------------------------------------------------------------------##
##Creating a folder at the root of the program
def make_sure_path_exists(path):
    try: os.makedirs(path)
    except OSError as exception:
        if exception.errno != errno.EEXIST:
            raise

##Фуfunction that translates from cm to pixel
def cm_in_px(cm):
    global px
    px = int(cm) * 38
    return px

##Creating a folder at the root of the program-------------------------------------------------------------------------#
make_sure_path_exists('Edited photos')
#----------------------------------------------------------------------------------------------------------------------#

##ФScale and store aspect ratio
def scale_image(input_image_path,
                output_image_path,
                width=None,
                height=None
                ):
    original_image = Image.open(input_image_path)
    w, h = original_image.size
    print('The original image size is {wide} wide x {height} '
          'high'.format(wide=w, height=h))

    if width and height:
        max_size = (width, height)
    elif width:
        max_size = (width, h)
    elif height:
        max_size = (w, height)
    else:
        # No width or height specified
        raise RuntimeError('Width or height required!')

    original_image.thumbnail(max_size, Image.ANTIALIAS)
    original_image.save(output_image_path)

    scaled_image = Image.open(output_image_path)
    width, height = scaled_image.size
    print('The scaled image size is {wide} wide x {height} '
          'high'.format(wide=width, height=height))
#----------------------------------------------------------------------------------------------------------------------#

##Signature checkbox 2
def chek_cb2():
    global message_entry
    if ismarried2.get() == 0:
        message_entry = Entry(window, state=DISABLED, bd=2, width = 48)
        message_entry.place(x=10, y=275)
        return Label(window, text='Add ending to file name:', font=('Arial', 10)).place(x=10, y=250)
    else:
        message_entry = Entry(window, state=NORMAL, bd=2, width = 48)
        message_entry.place(x=10, y=275)
        return Label(window, text='Add ending to file name:', font=('Arial', 10)).place(x=10, y=250)
#----------------------------------------------------------------------------------------------------------------------#

##file renaming
def rename():
    global last_name
    if ismarried2.get() == 1:
        last_name = '_' + message_entry.get()
    else:
        last_name = ''
    return last_name
#----------------------------------------------------------------------------------------------------------------------#

#----------------------------------------------------------------------------------------------------------------------#
##Path to upload images
number_f = 0
def clicked_dialogOpen():
    global choosefile
    global number_f
    choosefile = filedialog.askopenfilename(multiple=True, parent = window, filetypes=(("Image files", "*.png"), ("all files", "*.*")))
    number_f = len(choosefile)
    label_file()
#----------------------------------------------------------------------------------------------------------------------#
##Check for characters in the string
def check_name():
    global d
    d = 0
    for i in message_entry.get():
        if i.isalpha():
            d += 1
        elif i.isdigit():
            d+= 1
        else:
            d+= 1
    return d
#----------------------------------------------------------------------------------------------------------------------#
##Display information about the number of selected images
def label_file():

    if number_f == 0:
        lbl2 = Label(window, text="Image not selected", font=('Arial', 9), fg = 'red')
        lbl2.place(x=start_pos_x, y=start_pos_y + step_des * 0.9)
    elif number_f == 1:
        lbl2 = Label(window, text='File selected             '.format(number_f), font=('Arial', 9), fg = 'green')
        lbl2.place(x=start_pos_x, y=start_pos_y + step_des * 0.9)
    else:
        lbl2 = Label(window, text='Files selected - {}       '.format(number_f), font=('Arial', 9), fg = 'green')
        lbl2.place(x=start_pos_x, y=start_pos_y + step_des * 0.9)
#----------------------------------------------------------------------------------------------------------------------#
##Open the path to save the file
filename = 0
def browse_button():
    global filename
    filename = filedialog.askdirectory()
    label_folder()
#----------------------------------------------------------------------------------------------------------------------#
##Display save directory information
def label_folder():

    if filename == 0:
        lbl = Label(window, text="Path not selected", font=('Arial', 9), fg = 'red')
        lbl.place(x=start_pos_x, y=start_pos_y + step_des * 2.3)
    elif filename == '':
        lbl = Label(window, text=os.getcwd() + '/Edited photos/', font=('Arial', 9), fg = 'green')
        lbl.place(x=start_pos_x, y=start_pos_y + step_des * 2.3)
    else:
        lbl = Label(window, text=filename, font=('Arial', 9), fg = 'green')
        lbl.place(x=start_pos_x, y=start_pos_y + step_des * 2.3)
#----------------------------------------------------------------------------------------------------------------------#
##Zoom and record an image
def scale():
    check_name()
    if number_f < 1:
        messagebox.showerror("Error", "No file selected")
    if ismarried2.get() == 1 and d < 1:
        messagebox.showerror("Error", "Parameter not entered: Add ending to file name")
    else:
        for i in range(number_f):
            try:
                with open(choosefile[i]) as im:
                    r = os.path.splitext(choosefile[i])
                    var = (os.path.basename(r[0]), r[1])

                    if filename == 0:
                        folder = os.getcwd() + '/Edited photos/'
                        output_name = folder + var[0] + rename() + var[1]
                        scale_image(input_image_path=choosefile[i],
                            output_image_path=output_name,
                            height=cm_in_px(message_cm.get()))

                        if ismarried.get() == 1:
                            image = Image.open(output_name)
                            if image.mode == 'RGBA':
                                r, g, b, a = image.split()
                                rgb_image = Image.merge('RGB', (r, g, b))
                                inverted_image = PIL.ImageOps.invert(rgb_image)
                                r2, g2, b2 = inverted_image.split()
                                final_transparent_image = Image.merge('RGBA', (r2, g2, b2, a))
                                final_transparent_image.save(output_name)
                            else:
                                inverted_image = PIL.ImageOps.invert(image)
                                inverted_image.save(output_name)

                    else:
                        folder = filename
                        output_name = filename + '/' + var[0] + rename() + var[1]
                        scale_image(input_image_path=choosefile[i],
                            output_image_path = output_name,
                            height=cm_in_px(message_cm.get()))

                        if ismarried.get() == 1:
                            image = Image.open(output_name)
                            if image.mode == 'RGBA':
                                r, g, b, a = image.split()
                                rgb_image = Image.merge('RGB', (r, g, b))
                                inverted_image = PIL.ImageOps.invert(rgb_image)
                                r2, g2, b2 = inverted_image.split()
                                final_transparent_image = Image.merge('RGBA', (r2, g2, b2, a))
                                final_transparent_image.save(output_name)
                            else:
                                inverted_image = PIL.ImageOps.invert(image)
                                inverted_image.save(output_name)
                    print("Çhose",choosefile[0])
            except:
                print('Error')
                messagebox.showerror("Error", "An error has occurred.\n\nThe program is intended for image processing only.\n\nContact the e-mail address:\nolehlastovetskyi99@gmail.com")
                quit()
        messagebox.showinfo("Message", "Completed!\nChanged {} files.\nFiles saved in the directory:\n{}".format(number_f, folder))
#----------------------------------------------------------------------------------------------------------------------#

##--------------------------------------------------------------------------------------------------------------------##

window = Tk()
ismarried = IntVar(value= 2)
ismarried.set(0)
ismarried2 = IntVar(value= 2)
ismarried2.set(0)
chek_cb2()
rename()
##------------------------------------------------------------------------------------------------------------------------##

start_pos_x = 10
start_pos_y = 10


height_button = 2
width_button = 32

font_button = ("Arial Bold", 11)
font_checkbox = ("Arial", 11)
font_combobox = ('Arial', 11)
font_label = ("Arial Bold", 11)
step_des = 60

label_folder()
label_file()

window.title("Scale")

btn_dialogOpen = Button(window, text="Image files", command=clicked_dialogOpen, height=height_button,
                        width=width_button, font=font_button)
btn_dialogOpen.place(x=start_pos_x, y=start_pos_y)

btn_browsebutton = Button(window, text="Save in folder", command=browse_button, height=height_button,
                          width=width_button, font=font_button)

btn_browsebutton.place(x=start_pos_x, y=start_pos_y + step_des + 25)

lbl = Label(window, text="Image height (cm):", font=font_label)
lbl.place(x=start_pos_x + 1, y=start_pos_y + step_des * 2.95)

message_cm = Entry(width=7)
message_cm.place(x=start_pos_x + 150, y=start_pos_y + step_des * 2.97)
message_cm.insert(0, "9")

ismarried_checkbutton = Checkbutton(text="Invert color", variable=ismarried, font =font_checkbox)
ismarried_checkbutton.place(x=start_pos_x + 1, y=start_pos_y + step_des * 3.4)

ismarried_checkbutton2 = Checkbutton(text="Rename the file", variable=ismarried2,
                                     font = font_checkbox, command = chek_cb2)
ismarried_checkbutton2.place(x=start_pos_x + 170, y=start_pos_y + step_des * 3.4)

btn_scale = Button(window, text="Convert", command=scale, height=height_button, width=width_button,
                   font=font_button, state = NORMAL)

btn_scale.place(x=start_pos_x, y=start_pos_y + step_des * 5.2)

Label(window, text='Github:', font=('Arial', 8)).place(x=10, y=385)

x = (window.winfo_screenwidth() - window.winfo_reqwidth()) / 2
y = (window.winfo_screenheight() - window.winfo_reqheight()) / 2
window.wm_geometry("+%d+%d" % (x, y))

window.maxsize(320,410)
window.minsize(320,410)
window.resizable(0, 0)

window.mainloop()

Ссылки на github

Комментарии (32)


  1. Hivemaster
    30.01.2022 21:48
    +6

    При чём тут big data?


    1. fedorro
      30.01.2022 21:59
      +4

      Первая версия этой программы могла обрабатывать один файл за раз, путь к которому задавался строкой в текстбоксе, а тут вон какая мега-автоматизация:

      Удобно добавлять файлы в программу

      Возможность добавлять сразу несколько изображений

      ????


      1. unsignedchar
        30.01.2022 22:08
        +4

        Big data это наверное что то другое :)


      1. Goupil
        31.01.2022 01:13

        Надо же. Код, который я написал вчера вокруг нейронки, тоже может в обработку батчей картинок, тысяч их . Могу с гордостью сказать что я тоже работаю с big data.


  1. Akon32
    30.01.2022 21:53
    +13

    Для таких вещей достаточно imagemagic + bash.


    1. unsignedchar
      30.01.2022 21:58
      +3

      У каждого есть наготове свой микроскоп ;) Но из однострочника на bash статья не получится.


  1. kAIST
    30.01.2022 22:11
    +10

    Не, я конечно тоже через подобное проходил - писал свой резайзер на питоне с блекджеком и куртизанками (поддержка многоядерности, заморочки с резкостью и пр.), но в голову не приходило писать об этом. Потому что это все было очень просто с точки зрения программирования и нужно только мне (как и размеры в сантиметрах с прибитым гвоздями странным dpi у вас).

    P.S. Советую начинать читать про классы в питоне, очень уж страшно выглядит код.


  1. Gaikotsu
    30.01.2022 23:26
    +4

    Судя по скринам, делалось для винды?

    Тогда, вместо изобретения велосипеда, вполне можно было просто воспользоваться к примеру xnview, позволяющим делать кучу разных действий над целыми папками с изображениями, притом разом можно делать целую цепочку действий над каждым файлом - то же самое изменение размера, инвертирование цветов, наложение фильтров и т.д. и т.п.


    1. DaneSoul
      31.01.2022 10:46

      XnViewMP вполне себе кросс-платформенный, не первый год использую под Linux.
      А так, действительно, мега мощный и удобный софт для работы с изображениями — и как просмотрщик и как пакетный обработчик.


      1. Gaikotsu
        31.01.2022 11:16

        А, ну я сам просто до сих пор пользуюсь classic-версией, которая имеется только для windows.


  1. Glomberg
    30.01.2022 23:34
    +8

    Хосспади, коллеги! Призываю поддержать велосипедостроение! Ведь только так мы проходим тернии и образумливаеся. Объективная критика усилит, безусловно, эффек.


    1. Interreto
      31.01.2022 00:23
      +2

      Афтар, перелогинся!


    1. Hivemaster
      31.01.2022 22:17

      Велосипедостроение - дело хорошее. Но писать о велоспедах на Хабр - это слишком.


  1. KarmicDude
    31.01.2022 00:09
    +1

    это делается все парой команд :\ зачем?


    1. vodopad
      31.01.2022 00:18
      +2

      Да ладно, статья может пригодится какому-нибудь студенту. Может лаба будет какая-нибудь похожая. Или новчику в Python.


      1. unsignedchar
        31.01.2022 11:49

        новчику в Python

        Ни в коем случае.


  1. z0ic
    31.01.2022 00:24

    Раньше это называлось бы скриптом.


    1. Metotron0
      31.01.2022 11:55
      +1

      Оно и сейчас скрипт. Программа на интерпретируемом языке.


  1. DonAgosto
    31.01.2022 00:26
    +11

    каждый начинающий хардварщик обязан сделать:
    1. Часики с будильником
    2. Погодную станцию (с часиками с будильником)

    каждый начинающий софтварщик обязан сделать:
    1. Прогу для пакетного ресайза картинок
    2. Прогу для пакетного поиска/замены текста (с опцией пакетного ресайза картинок)


    1. Goupil
      31.01.2022 01:14
      +2

      1. Выложить это на хабр прежде чем на гит хаб


    1. maximw
      31.01.2022 01:42
      +2

      3. Кнопку, которая от мышки убегает.


    1. rostislav-zp
      31.01.2022 21:49

      Собрав поголную станцию, сравнил ее с готовой китайской в красивом корпусе.стало жаль потраченных денег и времени.купил готовую,а коробка с запчастями так и валяется в шкафу.но было интересно конечно проходить весь путь


  1. bungu
    31.01.2022 01:00
    +6

    Ну и говнокод конечно. За global надо вообще палкой по пальцам бить. Про f-string, os.path.join() автор конечно не слышал. Вообщем смеялись всей маршруткой (отделом разработки)

    def cm_in_px(cm):
    global px
    px = int(cm) * 38
    return px

    Вот тут вообще не понял. А как же dpi?


    1. Metotron0
      31.01.2022 02:54
      +1

      Ну, это ведь Фуfunction, судя по комментарию


    1. Maccimo
      31.01.2022 03:48

      Ну какая палка, коллега, вы что? Мы же не в каменном веке!
      Для этого давно уже изобрели киянку.


  1. 13_beta2
    31.01.2022 02:01
    +4

    Выскажу крамолу, но по моим наблюдениям стабильно вызывают печаль большинство статей и "статей" с заголовком подходящим под маску %python%. Первопричина хайпа (так ещё говорят или новое слово изобрели?) вокруг языка понятна, причина низкого качества и кода и статей легко вмещается в понятие "новички". Однако как с этим бороться решительно не ясно. Пока сформировалось что-то вроде баннерной слепоты, но такой подход не решает самой проблемы, интересные материалы могут пройти мимо, случайные тапы, опять же.


    1. serginfo2009
      31.01.2022 08:11

      Давно пишу на Python и всем сердцем люблю этот язык. В свете изложенного статьи про Python на Хабре не читаю принципиально.


    1. HemulGM
      31.01.2022 08:15

      Не "дорос" до лайков. Поддержу комментарием.


    1. MentalBlood
      31.01.2022 09:40

      Специфичного решения действительно не видно, остается только плюсовать то что хорошо и минусовать то что плохо


  1. ertaquo
    31.01.2022 10:30

    Для Windows есть довольно удобный пакет программ PowerToys от самих Microsoft, который включает в себя утилиту для изменения размера изображений.

    Для *nix гораздо проще воспользоваться imagemagick.


  1. cadovvl
    31.01.2022 14:50
    +3

    Та ну что такое.

    Увидев синопсис, теги и оценку, пропустил статью и сразу полез в комментарии. А комментарии что-то не огонь.

    Пойду поищу статью про новую сортировку и "окончательные точки над И в вопросе производительности С# и C++". Там веселее.



  1. Myxach
    01.02.2022 21:20

    А кому надо изображение в см? Почему сразу не писать в пикселях размер?