備忘録として残しておきます。
目次
バージョン4
webp形式での出力に対応
import os
from tkinter import Tk, Label, Button, Entry, filedialog, StringVar, Checkbutton, BooleanVar, ttk
from tkinterdnd2 import DND_FILES, TkinterDnD
from PIL import Image, ImageFilter, ExifTags
import datetime
import pillow_heif
# HEIF形式サポートを登録
pillow_heif.register_heif_opener()
# 複数ファイルのパスを保持するグローバル変数
selected_files = []
def remove_exif_data(img):
"""
EXIFデータを除去してファイルサイズを削減
"""
try:
# EXIFデータがある場合は除去
if hasattr(img, '_getexif') and img._getexif() is not None:
# 画像の向きだけは保持
exif = img._getexif()
if exif is not None:
for tag, value in exif.items():
decoded = ExifTags.TAGS.get(tag, tag)
if decoded == 'Orientation':
# 画像の向きに基づいて回転
if value == 3:
img = img.rotate(180, expand=True)
elif value == 6:
img = img.rotate(270, expand=True)
elif value == 8:
img = img.rotate(90, expand=True)
break
# 新しい画像オブジェクトを作成(EXIFなし)
data = list(img.getdata())
img_without_exif = Image.new(img.mode, img.size)
img_without_exif.putdata(data)
return img_without_exif
except:
return img
def smart_resize(img, max_width, max_height):
"""
賢いリサイズ:画質を保ちながら最適なサイズに調整
"""
original_width, original_height = img.size
# 既に小さい場合はリサイズしない
if original_width <= max_width and original_height <= max_height:
return img
# アスペクト比を保持してリサイズ
width_ratio = max_width / original_width
height_ratio = max_height / original_height
ratio = min(width_ratio, height_ratio)
new_width = int(original_width * ratio)
new_height = int(original_height * ratio)
# Lanczosフィルターで高品質リサイズ
return img.resize((new_width, new_height), Image.LANCZOS)
def optimize_colors(img):
"""
色数最適化:視覚的品質を保ちながら色数を削減
"""
if img.mode == 'RGBA':
# アルファチャンネルがある場合は白背景に合成
background = Image.new('RGB', img.size, (255, 255, 255))
background.paste(img, mask=img.split()[-1])
img = background
elif img.mode == 'P':
# パレットモードの場合はRGBに変換
img = img.convert('RGB')
elif img.mode != 'RGB':
img = img.convert('RGB')
return img
def compress_and_resize_image(input_path, output_path, quality=85, max_width=1200, max_height=1200,
use_webp=False, remove_metadata=True, color_optimization=True):
"""
画像を高品質で圧縮する改善版関数
"""
try:
with Image.open(input_path) as img:
print(f"処理中: {os.path.basename(input_path)} ({img.size[0]}x{img.size[1]})")
# メタデータ除去
if remove_metadata:
img = remove_exif_data(img)
# 色最適化
if color_optimization:
img = optimize_colors(img)
# スマートリサイズ
img = smart_resize(img, max_width, max_height)
# WebP形式での保存
if use_webp and output_path.lower().endswith('.webp'):
# WebPは高い圧縮率と品質を両立
img.save(output_path, format='WEBP', quality=quality, method=6,
optimize=True, lossless=False)
else:
# JPEG保存(改善版)
# サブサンプリングを調整してより良い品質を実現
if quality >= 90:
subsampling = 0 # 高品質時は4:4:4
elif quality >= 75:
subsampling = 1 # 中品質時は4:2:2
else:
subsampling = 2 # 低品質時は4:2:0
img.save(output_path, format='JPEG', quality=quality,
optimize=True, progressive=True, subsampling=subsampling)
# ファイルサイズ情報を表示
original_size = os.path.getsize(input_path)
compressed_size = os.path.getsize(output_path)
compression_ratio = (1 - compressed_size / original_size) * 100
print(f"圧縮完了: {compression_ratio:.1f}% 削減 ({original_size:,} → {compressed_size:,} bytes)")
except Exception as e:
print(f"エラー: {input_path} - {e}")
raise e
def select_output_folder():
folder_path = filedialog.askdirectory()
if folder_path:
output_folder.set(folder_path)
def generate_output_filename(input_path, output_ext):
base_name = os.path.splitext(os.path.basename(input_path))[0]
timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
return f"{base_name}_compressed_{timestamp}{output_ext}"
def compress_image_gui():
if not selected_files:
result_label.config(text="エラー: ファイルが選択されていません")
return
# 出力形式の決定
output_ext = ".webp" if use_webp.get() else ".jpg"
try:
quality = int(compression_quality.get())
max_width = int(max_width_var.get())
max_height = int(max_height_var.get())
except Exception as e:
result_label.config(text=f"設定値が不正です: {e}")
return
output_files = []
total_original_size = 0
total_compressed_size = 0
progress_bar['maximum'] = len(selected_files)
progress_bar['value'] = 0
for i, input_path in enumerate(selected_files):
try:
original_size = os.path.getsize(input_path)
total_original_size += original_size
output_name = generate_output_filename(input_path, output_ext)
output_path = os.path.join(output_folder.get(), output_name)
compress_and_resize_image(
input_path, output_path,
quality=quality,
max_width=max_width,
max_height=max_height,
use_webp=use_webp.get(),
remove_metadata=remove_metadata.get(),
color_optimization=color_optimize.get()
)
compressed_size = os.path.getsize(output_path)
total_compressed_size += compressed_size
output_files.append(output_path)
progress_bar['value'] = i + 1
app.update_idletasks()
except Exception as e:
result_label.config(text=f"エラー: {input_path} - {e}")
return
# 全体の圧縮率を計算
overall_compression = (1 - total_compressed_size / total_original_size) * 100
result_label.config(text=f"圧縮完了! 全体で{overall_compression:.1f}%削減 ({len(output_files)}ファイル)")
def set_output_folder_from_file(file_path):
folder = os.path.dirname(file_path)
output_folder.set(folder)
def drop_file(event):
"""
ドラッグ&ドロップされたファイルのパスを取得し、複数ファイルの場合も対応する
"""
files = app.tk.splitlist(event.data)
valid_extensions = ['.jpg', '.jpeg', '.png', '.webp', '.heic', '.heif', '.bmp', '.tiff']
valid_files = []
for file_path in files:
file_path = file_path.strip('{}').replace('\\', '/')
if os.path.isfile(file_path) and os.path.splitext(file_path)[1].lower() in valid_extensions:
valid_files.append(file_path)
if valid_files:
global selected_files
selected_files = valid_files
input_file.set(f"{len(valid_files)}個のファイルが選択されました")
set_output_folder_from_file(valid_files[0])
result_label.config(text=f"選択されたファイル: {len(valid_files)}個")
else:
result_label.config(text="エラー: 有効な画像ファイルをドラッグしてください")
def select_input_files():
"""
ファイル選択ダイアログから複数のファイルを選択する
"""
file_paths = filedialog.askopenfilenames(
filetypes=[("画像ファイル", "*.jpg *.jpeg *.png *.webp *.heic *.heif *.bmp *.tiff")]
)
if file_paths:
global selected_files
selected_files = [fp.replace('\\', '/') for fp in file_paths]
input_file.set(f"{len(selected_files)}個のファイルが選択されました")
set_output_folder_from_file(selected_files[0])
result_label.config(text=f"選択されたファイル: {len(selected_files)}個")
# TkinterDnDを利用したウィンドウの生成
app = TkinterDnD.Tk()
app.title("高品質画像圧縮ツール v2.0")
app.geometry("600x500")
# ファイル選択部分
Label(app, text="ドラッグアンドドロップまたはボタンでファイルを選択:", font=("Arial", 10, "bold")).pack(pady=5)
input_file = StringVar()
Entry(app, textvariable=input_file, width=60, state="readonly").pack(pady=5)
Button(app, text="ファイルを選択", command=select_input_files).pack(pady=5)
app.drop_target_register(DND_FILES)
app.dnd_bind('<<Drop>>', drop_file)
# 出力フォルダ部分
Label(app, text="出力フォルダ:", font=("Arial", 10, "bold")).pack(pady=(10,5))
output_folder = StringVar()
Entry(app, textvariable=output_folder, width=60).pack(pady=5)
Button(app, text="フォルダを選択", command=select_output_folder).pack(pady=5)
# 設定部分
settings_frame = ttk.LabelFrame(app, text="圧縮設定", padding=10)
settings_frame.pack(pady=10, padx=20, fill="x")
# 品質設定
quality_frame = ttk.Frame(settings_frame)
quality_frame.pack(fill="x", pady=2)
Label(quality_frame, text="圧縮品質(1〜100):").pack(side="left")
compression_quality = StringVar(value="85")
Entry(quality_frame, textvariable=compression_quality, width=10).pack(side="right")
# サイズ設定
size_frame = ttk.Frame(settings_frame)
size_frame.pack(fill="x", pady=2)
Label(size_frame, text="最大幅:").pack(side="left")
max_width_var = StringVar(value="1920")
Entry(size_frame, textvariable=max_width_var, width=10).pack(side="right", padx=(0,10))
Label(size_frame, text="最大高さ:").pack(side="right")
max_height_var = StringVar(value="1080")
Entry(size_frame, textvariable=max_height_var, width=10).pack(side="right")
# オプション設定
options_frame = ttk.Frame(settings_frame)
options_frame.pack(fill="x", pady=5)
use_webp = BooleanVar(value=False)
Checkbutton(options_frame, text="WebP形式で出力(より高い圧縮率)", variable=use_webp).pack(anchor="w")
remove_metadata = BooleanVar(value=True)
Checkbutton(options_frame, text="メタデータを除去", variable=remove_metadata).pack(anchor="w")
color_optimize = BooleanVar(value=True)
Checkbutton(options_frame, text="色最適化", variable=color_optimize).pack(anchor="w")
# 実行ボタン
Button(app, text="画像を圧縮", command=compress_image_gui,
font=("Arial", 12, "bold"), bg="#4CAF50", fg="white").pack(pady=15)
# プログレスバー
progress_bar = ttk.Progressbar(app, mode='determinate')
progress_bar.pack(pady=5, padx=20, fill="x")
# 結果表示
result_label = Label(app, text="", wraplength=500)
result_label.pack(pady=10)
app.mainloop()
バージョン3
複数ファイルの同時圧縮に対応
import os
from tkinter import Tk, Label, Button, Entry, filedialog, StringVar
from tkinterdnd2 import DND_FILES, TkinterDnD
from PIL import Image
import datetime
# 複数ファイルのパスを保持するグローバル変数
selected_files = []
def compress_and_resize_image(input_path, output_path, quality=85, max_width=1200):
"""
画像を圧縮し、必要に応じてリサイズしてJPEG形式で保存する関数
*保存時に最適化(optimize)とプログレッシブ(progressive)オプションを利用し、
不要なメタデータを除去することで容量を削減します。
"""
try:
with Image.open(input_path) as img:
# アスペクト比を保持してリサイズ(幅がmax_widthを超える場合)
if img.width > max_width:
aspect_ratio = img.height / img.width
new_width = max_width
new_height = int(new_width * aspect_ratio)
img = img.resize((new_width, new_height), Image.LANCZOS)
# JPEG形式はRGBモードのみサポートしているため変換
if img.mode != 'RGB':
img = img.convert('RGB')
# JPEG保存時に最適化およびプログレッシブオプションを有効にする
# ※qualityは1〜100で指定(数値が大きいほど画質が良く、容量は大きくなります)
img.save(output_path, format='JPEG', quality=quality, optimize=True, progressive=True)
except Exception as e:
print(f"エラー: {e}")
def select_output_folder():
folder_path = filedialog.askdirectory()
if folder_path:
output_folder.set(folder_path)
def generate_output_filename(input_path, output_ext):
base_name = os.path.splitext(os.path.basename(input_path))[0]
timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
return f"{base_name}_compressed_{timestamp}{output_ext}"
def compress_image_gui():
if not selected_files:
result_label.config(text="エラー: ファイルが選択されていません")
return
output_ext = ".jpg" # 出力は常にJPEG
try:
quality = int(compression_quality.get())
except Exception as e:
result_label.config(text=f"圧縮クオリティの値が不正です: {e}")
return
output_files = []
for input_path in selected_files:
output_name = generate_output_filename(input_path, output_ext)
output_path = os.path.join(output_folder.get(), output_name)
compress_and_resize_image(input_path, output_path, quality=quality, max_width=1200)
output_files.append(output_path)
result_label.config(text=f"圧縮が完了しました: {', '.join(output_files)}")
def set_output_folder_from_file(file_path):
folder = os.path.dirname(file_path)
output_folder.set(folder)
def drop_file(event):
"""
ドラッグ&ドロップされたファイルのパスを取得し、複数ファイルの場合も対応する
"""
# event.dataには1つまたは複数のファイルパスが含まれる
files = app.tk.splitlist(event.data)
valid_extensions = ['.jpg', '.jpeg', '.png', '.webp']
valid_files = []
for file_path in files:
# ファイルパスに空白が含まれる場合、波括弧で囲まれていることがあるので除去
file_path = file_path.strip('{}').replace('\\', '/')
if os.path.isfile(file_path) and os.path.splitext(file_path)[1].lower() in valid_extensions:
valid_files.append(file_path)
if valid_files:
global selected_files
selected_files = valid_files
input_file.set(', '.join(valid_files))
set_output_folder_from_file(valid_files[0])
result_label.config(text=f"選択されたファイル: {', '.join(valid_files)}")
else:
result_label.config(text="エラー: 有効な画像ファイルをドラッグしてください")
def select_input_files():
"""
ファイル選択ダイアログから複数のファイルを選択する
"""
file_paths = filedialog.askopenfilenames(filetypes=[("画像ファイル", "*.jpg *.jpeg *.png *.webp")])
if file_paths:
global selected_files
selected_files = [fp.replace('\\', '/') for fp in file_paths]
input_file.set(', '.join(selected_files))
set_output_folder_from_file(selected_files[0])
result_label.config(text=f"選択されたファイル: {', '.join(selected_files)}")
# TkinterDnDを利用したウィンドウの生成
app = TkinterDnD.Tk()
app.title("画像圧縮ツール")
app.geometry("500x400")
Label(app, text="ドラッグアンドドロップでファイルを選択:").pack(pady=5)
input_file = StringVar()
Entry(app, textvariable=input_file, width=50).pack(pady=5)
Button(app, text="ファイルを選択", command=select_input_files).pack(pady=5)
app.drop_target_register(DND_FILES)
app.dnd_bind('<<Drop>>', drop_file)
Label(app, text="出力フォルダ:").pack(pady=5)
output_folder = StringVar()
Entry(app, textvariable=output_folder, width=50).pack(pady=5)
Button(app, text="フォルダを選択", command=select_output_folder).pack(pady=5)
Label(app, text="圧縮クオリティ(1〜100):").pack(pady=5)
compression_quality = StringVar(value="60")
Entry(app, textvariable=compression_quality, width=10).pack(pady=5)
# 出力形式の選択ウィジェットは削除(出力は常にJPEG)
Button(app, text="画像を圧縮", command=compress_image_gui).pack(pady=10)
result_label = Label(app, text="")
result_label.pack(pady=10)
app.mainloop()
バージョン2
PNGの読み込みに対応
import os
from tkinter import Tk, Label, Button, Entry, filedialog, StringVar
from tkinterdnd2 import DND_FILES, TkinterDnD
from PIL import Image
import datetime
def compress_and_resize_image(input_path, output_path, quality=85, max_width=1200):
"""
画像を圧縮し、必要に応じてリサイズしてJPEG形式で保存する関数
"""
try:
with Image.open(input_path) as img:
# アスペクト比を保持してリサイズ(幅がmax_widthを超える場合)
if img.width > max_width:
aspect_ratio = img.height / img.width
new_width = max_width
new_height = int(new_width * aspect_ratio)
img = img.resize((new_width, new_height), Image.LANCZOS)
# JPEG形式はRGBモードのみサポートしているため変換
if img.mode != 'RGB':
img = img.convert('RGB')
# JPEG形式で画像を保存
img.save(output_path, format='JPEG', quality=quality)
except Exception as e:
print(f"エラー: {e}")
def select_output_folder():
folder_path = filedialog.askdirectory()
if folder_path:
output_folder.set(folder_path)
def generate_output_filename(input_path, output_ext):
base_name = os.path.splitext(os.path.basename(input_path))[0]
timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
return f"{base_name}_compressed_{timestamp}{output_ext}"
def compress_image_gui():
input_path = input_file.get()
output_ext = ".jpg" # 出力は常にJPEG
output_name = generate_output_filename(input_path, output_ext)
output_path = os.path.join(output_folder.get(), output_name)
try:
quality = int(compression_quality.get())
compress_and_resize_image(input_path, output_path, quality=quality, max_width=1200)
result_label.config(text=f"圧縮が完了しました: {output_path}")
except Exception as e:
result_label.config(text=f"エラー: {e}")
def set_output_folder_from_file(file_path):
folder = os.path.dirname(file_path)
output_folder.set(folder)
def drop_file(event):
file_path = event.data.strip().strip('{}').replace('\\', '/')
valid_extensions = ['.jpg', '.jpeg', '.png', '.webp']
if os.path.isfile(file_path) and os.path.splitext(file_path)[1].lower() in valid_extensions:
input_file.set(file_path)
set_output_folder_from_file(file_path)
result_label.config(text=f"選択されたファイル: {file_path}")
else:
result_label.config(text="エラー: 有効な画像ファイルをドラッグしてください")
def select_input_file():
file_path = filedialog.askopenfilename(filetypes=[("画像ファイル", "*.jpg *.jpeg *.png *.webp")])
if file_path:
input_file.set(file_path)
set_output_folder_from_file(file_path)
# TkinterDnDを利用したウィンドウの生成
app = TkinterDnD.Tk()
app.title("画像圧縮ツール")
app.geometry("500x400")
Label(app, text="ドラッグアンドドロップでファイルを選択:").pack(pady=5)
input_file = StringVar()
Entry(app, textvariable=input_file, width=50).pack(pady=5)
app.drop_target_register(DND_FILES)
app.dnd_bind('<<Drop>>', drop_file)
Label(app, text="出力フォルダ:").pack(pady=5)
output_folder = StringVar()
Entry(app, textvariable=output_folder, width=50).pack(pady=5)
Button(app, text="フォルダを選択", command=select_output_folder).pack(pady=5)
Label(app, text="圧縮クオリティ(1〜100):").pack(pady=5)
compression_quality = StringVar(value="70")
Entry(app, textvariable=compression_quality, width=10).pack(pady=5)
# 出力形式の選択ウィジェットは削除(出力は常にJPEG)
Button(app, text="画像を圧縮", command=compress_image_gui).pack(pady=10)
result_label = Label(app, text="")
result_label.pack(pady=10)
app.mainloop()
バージョン1
import os
from tkinter import Tk, Label, Button, Entry, filedialog, StringVar
from tkinterdnd2 import DND_FILES, TkinterDnD
from PIL import Image
import datetime
def compress_and_resize_image(input_path, output_path, quality=85, max_width=1200):
"""
画像を圧縮し、必要に応じてリサイズする関数
Parameters:
input_path (str): 入力画像のパス
output_path (str): 出力画像の保存パス
quality (int): 圧縮クオリティ(1〜100)
max_width (int): 最大幅(超える場合リサイズ)
"""
try:
with Image.open(input_path) as img:
# アスペクト比を保持してリサイズ
if img.width > max_width:
aspect_ratio = img.height / img.width
new_width = max_width
new_height = int(new_width * aspect_ratio)
img = img.resize((new_width, new_height), Image.LANCZOS)
# 出力形式の決定
ext = os.path.splitext(output_path)[1].lower()
if ext in ['.jpg', '.jpeg']:
img_format = 'JPEG'
elif ext == '.png':
img_format = 'PNG'
elif ext == '.webp':
img_format = 'WEBP'
else:
raise ValueError(f"サポートされていない形式: {ext}")
# 画像を保存(圧縮適用)
img.save(output_path, format=img_format, quality=quality)
except Exception as e:
print(f"エラー: {e}")
def select_output_folder():
folder_path = filedialog.askdirectory()
if folder_path:
output_folder.set(folder_path)
def generate_output_filename(input_path, output_ext):
base_name = os.path.splitext(os.path.basename(input_path))[0]
timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
return f"{base_name}_compressed_{timestamp}{output_ext}"
def compress_image_gui():
input_path = input_file.get()
output_ext = f".{output_format.get().lower()}"
output_name = generate_output_filename(input_path, output_ext)
output_path = os.path.join(output_folder.get(), output_name)
try:
quality = int(compression_quality.get())
compress_and_resize_image(input_path, output_path, quality=quality, max_width=1200)
result_label.config(text=f"圧縮が完了しました: {output_path}")
except Exception as e:
result_label.config(text=f"エラー: {e}")
def set_output_folder_from_file(file_path):
folder = os.path.dirname(file_path)
output_folder.set(folder)
def drop_file(event):
file_path = event.data.strip().strip('{}').replace('\\', '/')
valid_extensions = ['.jpg', '.jpeg', '.png', '.webp']
if os.path.isfile(file_path) and os.path.splitext(file_path)[1].lower() in valid_extensions:
input_file.set(file_path)
set_output_folder_from_file(file_path)
result_label.config(text=f"選択されたファイル: {file_path}")
else:
result_label.config(text="エラー: 有効な画像ファイルをドラッグしてください")
def select_input_file():
file_path = filedialog.askopenfilename(filetypes=[("画像ファイル", "*.jpg *.jpeg *.png *.webp")])
if file_path:
input_file.set(file_path)
set_output_folder_from_file(file_path)
app = TkinterDnD.Tk()
app.title("画像圧縮ツール")
app.geometry("500x400")
Label(app, text="ドラッグアンドドロップでファイルを選択:").pack(pady=5)
input_file = StringVar()
Entry(app, textvariable=input_file, width=50).pack(pady=5)
app.drop_target_register(DND_FILES)
app.dnd_bind('<<Drop>>', drop_file)
Label(app, text="出力フォルダ:").pack(pady=5)
output_folder = StringVar()
Entry(app, textvariable=output_folder, width=50).pack(pady=5)
Button(app, text="フォルダを選択", command=select_output_folder).pack(pady=5)
Label(app, text="圧縮クオリティ(1〜100):").pack(pady=5)
compression_quality = StringVar(value="85")
Entry(app, textvariable=compression_quality, width=10).pack(pady=5)
Label(app, text="出力形式(jpg, png, webp):").pack(pady=5)
output_format = StringVar(value="jpg")
Entry(app, textvariable=output_format, width=10).pack(pady=5)
Button(app, text="画像を圧縮", command=compress_image_gui).pack(pady=10)
result_label = Label(app, text="")
result_label.pack(pady=10)
app.mainloop()
(以下2つのインストールも必要)
pip install pillow
pip install tkinterdnd2

コメント