MENU

画像を圧縮するPythonスクリプト

備忘録として残しておきます。

目次

バージョン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
よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)

目次