3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Python コードで画像に枠線をつけるコードを使って GUI アプリにしてみた。

Last updated at Posted at 2023-10-23

前回作成した「Python コードで画像に枠線をつける」Python コードを毎回実行するのが面倒になったので、 GUI アプリにしてみました。

前回の記事は下記からご覧ください。

Github にプロジェクトを公開しています。
よろしければご利用ください。

環境

今回の環境は下記のとおりです。

開発端末

  • OS: Windows 11 Pro
  • CPU: Intel Core i7-9750H
  • メモリ: 64GB
  • GPU: NVIDIA GeForce RTX 2070 Max-Q

Python 環境

  • Python: 3.11.0
  • ライブラリ
    • pillow
    • webcolors
    • tkinterdnd2
    • pyinstaller

コードの紹介

画像に枠線をつけるコードは前回と同じものになります。

ライブラリのインストール

ライブラリのインストールはお使いの環境にあわせて行ってください。

pip の場合

pip install pillow
pip install webcolors
pip install tkinterdnd2
pip install pyinstaller

poetry の場合

事前に pyproject.toml の Python バージョンを修正する必要があります。

[tool.poetry.dependencies]
- python = "^=3.11"
+ python = ">=3.11,<3.13"

修正を行い、上書き保存したら、モジュールのインストールを行います。

poetry add pillow
poetry add webcolors
poetry add tkinterdnd2
poetry add pyinstaller

Poetry について
Poetry の詳細は、下記の記事を参考にしてください。
https://qiita.com/IoriGunji/items/290db948c11fdc81046a

ソースコード

Python のソースコードは下記のとおりです。
image_liner.py については前回のコードを参考にしてください。

gui.py
import sys
import re
import configparser
import tkinter as tk
import tkinter.messagebox as mb
from tkinterdnd2 import DND_FILES, TkinterDnD
from imageliner import image_liner

def main():
    config = read_config()
    gui(config)

# GUI ウィンドウ
def gui(config):
    # ウィンドウ設定
    root = TkinterDnD.Tk()
    root.title('ImageLiner')
    root.geometry('330x100')
    default_color = root.cget("bg")

    # 変数宣言
    color = tk.StringVar()
    color.set(config['color'])
    size = tk.StringVar()
    size.set(config['size'])

    # ボーダー設定
    border_frame = tk.Frame(root, height=80, width=150, pady=10, padx=10, relief=tk.SOLID, bg=default_color, bd=1)
    border_frame.place(x=10, y=10)

    border_color_label = tk.Label(border_frame, text='Color:')
    border_color_label.place(x=0, y=10)
    border_color_input = tk.Entry(border_frame, textvariable=color, readonlybackground='white', justify=tk.CENTER)
    border_color_input.configure(validate='key', vcmd=(border_color_input.register(pre_validation_color), '%s', '%P'))
    border_color_input.place(x=50, y=10, width=60, height=21)

    border_size_label = tk.Label(border_frame, text='Size  :')
    border_size_label.place(x=0, y=35)
    border_size_input = tk.Entry(border_frame, textvariable=size, readonlybackground='white', justify=tk.CENTER)
    border_size_input.configure(validate='key', vcmd=(border_size_input.register(pre_validation_size), '%s', '%P'))
    border_size_input.place(x=50, y=35, width=60, height=21)
    border_size_unit = tk.Label(border_frame, text='px')
    border_size_unit.place(x=110, y=35)

    # ドロップエリア
    drop_frame = tk.Frame(root, height=80, width=150, pady=10, padx=10, relief=tk.SOLID, bg=default_color, bd=1)
    drop_frame.drop_target_register(DND_FILES)
    drop_frame.dnd_bind('<<Drop>>', lambda e: drop(e.data, color.get(), size.get()))
    drop_frame.place(x=170, y=10)

    drop_label = tk.Label(drop_frame, text='* Drop image file here')
    drop_label.place(x=65, y=30, anchor=tk.CENTER)

    # 各種タイトル
    border_label = tk.Label(root, text='Border settings')
    border_label.place(x=20, y=0)
    dorp_label = tk.Label(root, text='Drop files')
    dorp_label.place(x=180, y=0)

    # 閉じるボタンの制御
    root.protocol("WM_DELETE_WINDOW", lambda: exit(color.get(), size.get()))

    # GUI描画
    root.mainloop()

# 画像処理
def drop(files, color, size):
    try:
        validation_color(color)
        validation_size(size)
    except Exception as e:
        mb.showerror('Error', e)
        return

    files = dnd2_parse_files(files)
    color = '#' + color
    size = int(size)

    for file in files:
        for ext in image_liner.IMAGE_EXTS:
            if re.search(f'\.{ext}$', file):
                image_liner.image_edit(file, size, color)

# DnD2 ファイルパスの解析
def dnd2_parse_files(files_str):
    start = 0
    length = len(files_str)
    files = []
    while start < length:
        if files_str[start] == '{':
            end = files_str.find('}', start+1)
            file = files_str[start+1 : end]
            start = end + 2
        else:
            end = files_str.find(' ', start)
            if end < 0:
                file = files_str[start:]
                start = length
            else:
                file = files_str[start : end]
                start = end + 1
        files.append(file)
    return files

# 終了処理
def exit(color, size):
    try:
        validation_color(color)
        validation_size(size)
    except Exception as e:
        print(e)
        sys.exit()
    save_config(color, size)
    sys.exit()

# バリデーションチェック
def pre_validation_color(before_word, after_word):
    return (len(after_word) <= 6 or len(after_word) == 0)

def pre_validation_size(before_word, after_word):
    return ((after_word.isdecimal() or after_word == '') and (len(after_word) <= 4 or len(after_word) == 0))

def validation_color(color):
    if len(color) != 6:
        raise ValueError("Color code is not 6 digits!")
    if not re.match(r'^[0-9a-fA-F]{6}$', color):
        raise ValueError("Color code is invalid!")

def validation_size(size):
    if not size.isdecimal():
        raise ValueError("Size is not a number!")
    if int(size) <= 0:
        raise ValueError("Size is invalid!")

# 設定の読み込み
def read_config():
    config = configparser.ConfigParser()
    config.read('config.ini', encoding='utf-8')
    default = config['DEFAULT']
    configs = {
        'color': default.get('color') if default.get('color') != None else ''
        , 'size': default.get('size') if default.get('size') != None else ''
    }
    return configs

# 設定の保存
def save_config(color, size):
    config = configparser.ConfigParser()
    config.set('DEFAULT', 'color', color)
    config.set('DEFAULT', 'size', size)
    with open('config.ini', 'w', encoding='utf-8') as configfile:
        config.write(configfile)

if __name__ == "__main__":
    main()

ディレクトリ構成

ディレクトリ構成は下記のようになっています。

└ プロジェクトディレクトリ
 └ imageliner
  ├ gui.py
  └ image_liner.py

使い方

  1. gui.py を実行すると下記のような GUI が表示されます。
    imageliner_gui_01.png

  2. 枠線の設定を行います。

    • Color にカラーコードを指定します。(例:808080
    • Size に枠線の太さを指定します。(例:1
  3. Drop files の枠線内に加工したい画像をドラッグドロップします。

  4. 変換された画像は、元画像と同一ディレクトリ内の ./resized フォルダに出力されます。

PyInstaller

PyInstaller で exe ファイルにビルドする方法を紹介します。

hook-tkinterdnd2.py のダウンロード

今回はモジュールに TkinterDnD2 を利用しているため PyInstaller を利用する場合は、 hook-tkinterdnd2.py が必要になります。
Github からダウンロードして利用してください。

hook-tkinterdnd2.pygui.pyimage_liner.py のスクリプトと同一のディレクトリに保存します。

ディレクトリ構成

ディレクトリ構成は下記のようになっています。

└ プロジェクトディレクトリ
 └ imageliner
  ├ gui.py
  ├ image_liner.py
  └ hook-tkinterdnd2.py

PyInstaller の実行

PyInstaller で実行ファイルを作成する場合は、次のオプションを追加します。

--additional-hooks-dir [hook-tkinterdnd2.py が保存されているディレクトリのパス]

pip の場合

pyinstaller ./imageliner/gui.py --name ImageLiner --onefile --noconsole --collect-data tkinterdnd2 --additional-hooks-dir ./imageliner

Poetry の場合

poetry run pyinstaller ./imageliner/gui.py --name ImageLiner --onefile --noconsole --collect-data tkinterdnd2 --additional-hooks-dir ./imageliner

参考文献

下記のサイトを参考にさせて頂きました。

3
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?