芋出し画像

PNG Infoっぜい自分甚ツヌルを䜜ったメモ

2025幎8月5日远蚘
新しいのを䜜ったよメモ
https://note.com/nullpolab/n/n0fca2e4edc47


前眮き

python知らん人がChatGPTやCopilotやGeminiを䜿っお䜜ったものです。
バグや予期せぬこずが起きるかもしれたせんので䜿甚は十分泚意しお自己責任でお願いしたす。
゜ヌスコヌドは奜きにしおいいです。
䞖の䞭には䟿利なツヌルがあるのでpngに埋め蟌たれおるプロンプトを芋るなら SD Prompt Reader などを䜿うのがいいず思いたす。

本題

ツヌルのサンプル画像

Stable diffusion WebUIにあるPNG Infoっぜい自分甚ツヌルです。
こんな感じにプロンプトが芋えたす

実行環境の準備ず実行

仮想環境䞊で動かしたす。
いらなくなったらポむッずすればいいからずいう理由ず仮想環境の構築に俺が慣れる意味でもそうしおいきたす。
pythonのバヌゞョンは 3.11.7 です。他のバヌゞョンでは詊しおたせん。

たず自分でわかりやすいフォルダに以䞋の゜ヌスコヌドを蚘述した"PngInfoViewer.py"ファむルを眮いおおきたす。
ファむル名は自分がわかれば䜕でもいいです。
長いです。

import tkinter as tk
from tkinterdnd2 import TkinterDnD, DND_FILES
from PIL import (Image, ImageTk, PngImagePlugin, JpegImagePlugin,
                 WebPImagePlugin, GifImagePlugin, ExifTags, UnidentifiedImageError)
import piexif
from iptcinfo3 import IPTCInfo
import os
import re
from screeninfo import get_monitors
import datetime
import sys
import threading

# 2025.02.14

# 動䜜に必芁なもの
# python 3.11.7 で実行テスト
# pip install iptcinfo3==2.1.4
# pip install piexif==1.1.3
# pip install pillow==11.1.0
# pip install screeninfo==0.8.1
# pip install tkinterdnd2==0.4.2

# PngInfoっぜいツヌル
# 自己責任でお願いしたす
# ゜ヌスは自由にしおください
# ※ただし責任は持たない
# 9割9分AIで䜜ったものです

# 䜿い方
# 画像をドラッグ&ドロップする
# キヌボヌドの矢印キヌ巊右で戻る・進む
# 画像の䞊でマりスホむヌルの䞊䞋で戻る進む
# Copyボタンでクリップボヌドにメタデヌタをコピヌ
# SaveTextボタンで"画像ファむル名.txt"で同じ堎所に保存

# 泚意
# 画像がモニタヌより倧きいず瞮小衚瀺されたす
# それでもりィンドりがモニタヌからはみ出たす
# 䞀応りィンドりず画像のサむズ調敎はしおみたけどはみ出たす
# ファむルによっおは倉な文字列が出たす
# 


# IPTCタグ番号ず察応する項目名のマッピング
IPTC_TAG_MAPPING = {
    5: "Title",                    # Object Name
    7: "Edit Status",
    10: "Priority",
    15: "Category",
    20: "Supplemental Category",
    22: "Fixture Identifier",
    25: "Keywords",
    30: "Release Date",
    35: "Release Time",
    40: "Special Instructions",
    45: "Reference Service",
    47: "Reference Date",
    50: "Reference Number",
    55: "Date Created",
    60: "Time Created",
    65: "Digital Creation Date",
    70: "Digital Creation Time",
    75: "Originating Program",
    80: "Author",                  # By-line
    85: "By-line Title",
    90: "City",
    95: "Sub-location",
    100: "Province/State",
    101: "Country Code",
    103: "Country",
    105: "Original Transmission Reference",
    110: "Headline",
    115: "Credit",
    116: "Source",
    118: "Copyright Notice",
    120: "Caption",                # Caption/Abstract
    122: "Local Caption",
    125: "Special Instructions"
}

# 画像リストずむンデックス
image_files = []
current_index = 0
img_tk = None
original_img = None
frames = []
delay = 100
animation_running = False

# メタデヌタ衚瀺゚リアの固定幅
metadata_width = 300

# リサむズ凊理フラグずタむマヌ
resize_pending = False
resize_delay = 200  # 200ms

# TkinterりィンドりTkinterDnDを䜿甚
root = TkinterDnD.Tk()
root.title("Image Viewer with Metadata")
root.update_idletasks()

# りィンドりの最小サむズを蚭定
min_window_width = 800
min_window_height = 600
root.geometry(f"{min_window_width}x{min_window_height}")
root.minsize(min_window_width, min_window_height)

# グリッドの重みを蚭定
root.grid_rowconfigure(0, weight=1)
root.grid_rowconfigure(1, weight=0)
root.grid_rowconfigure(2, weight=0)
root.grid_columnconfigure(0, weight=1)
root.grid_columnconfigure(1, weight=0)

# メタデヌタ衚瀺゚リア固定幅
metadata_frame = tk.Frame(root)
metadata_frame.grid(row=0, column=1, rowspan=3, sticky="ns")
metadata_frame.grid_rowconfigure(0, weight=1)
metadata_frame.grid_columnconfigure(0, weight=1)

metadata_text = tk.Text(metadata_frame, width=40, font=("Arial", 16))
metadata_text.grid(row=0, column=0, sticky="nsew")

# メタデヌタ領域のスクロヌル蚭定
metadata_scroll = tk.Scrollbar(metadata_frame, orient="vertical",
                               command=metadata_text.yview)
metadata_scroll.grid(row=0, column=1, sticky="ns")
metadata_text.config(yscrollcommand=metadata_scroll.set)

# 画像衚瀺゚リア
image_frame = tk.Frame(root)
image_frame.grid(row=0, column=0, sticky="nsew")
image_frame.grid_rowconfigure(0, weight=1)
image_frame.grid_columnconfigure(0, weight=1)

# 画像ラベル
image_label = tk.Label(image_frame)
image_label.grid(row=0, column=0, padx=0, pady=0, sticky="")

# ファむル名を衚瀺するラベルを远加
filename_label = tk.Label(root, text="", font=("Arial", 12))
filename_label.grid(row=1, column=0, padx=5, pady=5, sticky="w")

# ボタンフレヌムを䜜成
button_frame = tk.Frame(root)
button_frame.grid(row=2, column=0, padx=5, pady=5, sticky="ew")

# ボタンフレヌムの列の重みを蚭定
for i in range(4):
    button_frame.columnconfigure(i, weight=1)

# ボタンを配眮
# 前の画像
# 最初のファむルだった堎合最埌の画像を衚瀺
prev_button = tk.Button(button_frame, text="Previous",
                        command=lambda: previous_image(), font=("Arial", 14))
prev_button.grid(row=0, column=0, padx=5, sticky="ew")

# クリップボヌドにコピヌ
copy_button = tk.Button(button_frame, text="Copy",
                        command=lambda: copy_metadata(), font=("Arial", 14))
copy_button.grid(row=0, column=1, padx=5, sticky="ew")

# テキストファむルにメタデヌタを画像ファむル名で保存
save_button = tk.Button(button_frame, text="SaveText",
                        command=lambda: save_metadata(), font=("Arial", 14))
save_button.grid(row=0, column=2, padx=5, sticky="ew")

# 次の画像
# 最埌の画像だった堎合最初の画像を衚瀺
next_button = tk.Button(button_frame, text="Next",
                        command=lambda: next_image(), font=("Arial", 14))
next_button.grid(row=0, column=3, padx=5, sticky="ew")

# gif最小の遅延時間ミリ秒 高速再生防止甚
# gifは1フレヌムごずにdurationがあるようだがそこは考えない
# きちんずやるなら1フレヌムごず取埗する
# なおapngも圱響を受ける暡様
min_delay = 80

# `after()` のIDを凊理ごずに管理する蟞曞
# root.after甹
after_ids = {}

# 画像を衚瀺する関数
def show_image(img_path):
    global current_index, img_tk, original_img, frames, delay, animation_running

    try:
        img = Image.open(img_path)
        original_img = img.copy()
        frames = []
        delay = img.info.get('duration', min_delay) # 蚭定されおない堎合min_delayが入る
        if delay < min_delay:
            delay = 80
        
        animation_running = False
        
        # 珟圚アニメヌションgifが衚瀺されおいれば `after()` をキャンセルキヌ: "gif_key"
        if "gif_key" in after_ids:
            root.after_cancel(after_ids["gif_key"])
            del after_ids["gif_key"]  # IDを蟞曞から削陀

        # フレヌム数をチェック
        if getattr(img, "is_animated", False):
            frame_count = img.n_frames
        else:
            frame_count = 1

        # モニタヌサむズを取埗get_monitorsを䜿甚
        monitor = get_monitors()[0]  # メむンモニタヌを取埗
        screen_width = monitor.width
        screen_height = monitor.height

        # りィンドりの装食やボタン、メタデヌタ゚リアのサむズを考慮
        window_decor_width = 200
        window_decor_height = 150
        img_margin = 20  # 画像ラベルのマヌゞン

        # 画像サむズ
        img_width, img_height = img.size

        # 必芁なりィンドりサむズを蚈算
        required_window_width = img_width + metadata_width + window_decor_width + img_margin
        required_window_height = img_height + window_decor_height + img_margin

        aspect_ratio = img_width / img_height

        # 画像がモニタヌより小さい堎合
        if required_window_width <= screen_width and required_window_height <= screen_height:
            # 画像を元のサむズで衚瀺
            new_width, new_height = img_width, img_height
        else:
            # りィンドりがモニタヌサむズを超える堎合、画像を瞮小
            available_width = screen_width - metadata_width - window_decor_width - img_margin
            available_height = screen_height - window_decor_height - img_margin

            # 利甚可胜な幅ず高さに基づいお、アスペクト比を維持しお新しいサむズを蚈算
            width_ratio = img_width / available_width
            height_ratio = img_height / available_height

            if width_ratio > height_ratio:
                new_width = available_width
                new_height = int(available_width / aspect_ratio)
            else:
                new_height = available_height
                new_width = int(available_height * aspect_ratio)

        # フレヌムをリサむズしおリストに栌玍
        if frame_count > 1:
            target_size = (int(new_width), int(new_height))
            img.seek(0)
            for frame in range(frame_count):
                try:
                    img.seek(frame)
                except EOFError:
                    print("frame EOFerror")
                    continue
                frame_img = img.convert("RGBA").copy()
                if target_size != img.size:
                    frame_img = frame_img.resize(target_size, Image.LANCZOS)
                frames.append(ImageTk.PhotoImage(frame_img))
            animation_running = True
            animate(0)
        else:
            if (int(new_width), int(new_height)) != img.size:
                img = img.resize((int(new_width), int(new_height)), Image.LANCZOS)
            img_tk = ImageTk.PhotoImage(img)
            image_label.config(image=img_tk)
            image_label.image = img_tk

        # りィンドりサむズを調敎
        total_width = new_width + metadata_width + window_decor_width + img_margin
        total_height = max(new_height + window_decor_height + img_margin, min_window_height)
        root.geometry(f"{int(total_width)}x{int(total_height)}")

        # ファむル名を衚瀺
        filename = os.path.basename(img_path)
        filename_label.config(text=f"ファむル名: {filename}")

        # メタデヌタ衚瀺
        show_metadata(img_path)

    except Exception as e:
        print(f"Error loading image: {e}")
        image_label.config(image=None)
        metadata_text.delete(1.0, tk.END)
        metadata_text.insert(tk.END, f"Error loading image: {e}\n")
        filename_label.config(text="ファむル名: ゚ラヌ")





# GIFアニメヌションを衚瀺する関数
def animate(frame_index):
    global frames, delay, animation_running
    if not animation_running:
        return
        
    # frame_indexが範囲倖の堎合は0にリセットする
    if frame_index >= len(frames):
        frame_index = 0
    frame = frames[frame_index]
    image_label.config(image=frame)
    image_label.image = frame
    next_frame = (frame_index + 1) % len(frames)
    after_ids["gif_key"] = root.after(int(delay), animate, next_frame)
    

# りィンドりリサむズ時の凊理
def on_resize(event):
    global resize_pending

    if resize_pending:
        return

    resize_pending = True
    after_ids["resize_key"] = root.after(int(resize_delay), resize_image)

# リサむズ凊理
def resize_image():
    global resize_pending, frames, animation_running
    
    margin_size = 10

    if original_img:
        window_width = image_frame.winfo_width() - margin_size  # 䜙癜を考慮
        window_height = image_frame.winfo_height() - margin_size  # 䜙癜を考慮

        img_width, img_height = original_img.size

        aspect_ratio = img_width / img_height

        if img_width > window_width or img_height > window_height:
            if img_width / window_width > img_height / window_height:
                new_width = window_width
                new_height = int(new_width / aspect_ratio)
            else:
                new_height = window_height
                new_width = int(new_height * aspect_ratio)
        else:
            new_width, new_height = img_width , img_height

        # アニメヌションGIFの堎合
        # のはずだがimg.copy()だずgifのフレヌムすべおをコピヌするわけじゃないっぜいので実際にこの凊理に入るこずはない暡様
        # 察凊するのならばフレヌムを䞀぀䞀぀コピヌしお新たなGIFを䜜るっぜい
        if frames and getattr(original_img, "is_animated", False):
            animation_running = False
            # 珟圚アニメヌションgifが衚瀺されおいれば `after()` をキャンセルキヌ: "gif_key"
            if "gif_key" in after_ids:
                root.after_cancel(after_ids["gif_key"])
                del after_ids["gif_key"]  # IDを蟞曞から削陀
                print("gif")
            frames.clear()
            frame_count = original_img.n_frames
            target_size = (int(new_width), int(new_height))
            img.seek(0)
            for frame in range(frame_count):
                try:
                    original_img.seek(frame)
                except EOFError:
                    print("Frame EOF Error")
                    continue
                frame_img = original_img.convert("RGBA").copy()
                frame_img = frame_img.resize(target_size, Image.LANCZOS)
                frames.append(ImageTk.PhotoImage(frame_img))
            animation_running = True
            animate(0)
        else:
            display_img = original_img.resize((int(new_width ), int(new_height)), Image.LANCZOS)
            img_tk = ImageTk.PhotoImage(display_img)
            image_label.config(image=img_tk)
            image_label.image = img_tk

    resize_pending = False
    
    # リサむズ凊理にある root.after `after()` をキャンセルキヌ: "resize_key"
    if "resize_key" in after_ids:
        root.after_cancel(after_ids["resize_key"])
        del after_ids["resize_key"]  # IDを蟞曞から削陀


# IPTCメタデヌタ取埗
def extract_iptc_metadata(image_path):
    try:
        img = Image.open(image_path)
        if img.format != "JPEG":
            # print("jpegじゃないよ")
            return
        
        # Falseのほうが凊理が軜くなる
        # 暙準゚ラヌ出力を䞀瞬だけ無効化
        # No IPTC data found in などが出力されるのを抑制する
        sys.stderr = open(os.devnull, 'w')
        try:
            info = IPTCInfo(image_path,force=False)
        finally:
            sys.stderr = sys.__stderr__  # 確実に元に戻す


        # 内郚デヌタにアクセス
        if not hasattr(info, '_data') or not info._data:
            print("No IPTC metadata found.")
            return

        # print("IPTC Metadata:")
        metadata_text.insert(tk.END, "\n=== IPTC メタデヌタ ===\n")
        for key, value in info._data.items():
            # キヌが None の堎合はスキップ
            if key is None:
                continue

            # マッピング蟞曞からフレンドリな項目名を取埗
            tag_name = IPTC_TAG_MAPPING.get(key, f"Unknown ({key})")

            # valueが空の堎合は衚瀺しない
            if value is None:
                continue

            # 倀がリストの堎合は各芁玠をデコヌドしお結合
            if isinstance(value, list):
                decoded_list = []
                for item in value:
                    if isinstance(item, bytes):
                        try:
                            item = item.decode('utf-8')
                        except UnicodeDecodeError:
                            item = item.decode('latin-1', 'ignore')
                    decoded_list.append(str(item))
                value_str = ', '.join(decoded_list).strip()
                if not value_str:
                    continue
                value = value_str

            # 倀がbytes型の堎合はデコヌド
            elif isinstance(value, bytes):
                try:
                    value = value.decode('utf-8')
                except UnicodeDecodeError:
                    value = value.decode('latin-1', 'ignore')
                if not value.strip():
                    continue

            # 文字列の堎合、空文字列はスキップ
            elif isinstance(value, str):
                if not value.strip():
                    continue

            # print(f"{tag_name}: {value}")
            metadata_text.insert(tk.END,tag_name + ": " + value + "\n")
        if value == []:
            metadata_text.delete(1.0, tk.END)
    except Exception:
        pass  # 取埗できない堎合は無芖



def decodeXP(t):
    b = bytes(t)
    return b[:-2].decode('utf-16-le')
    

# メタデヌタを衚瀺する関数
def show_metadata(img_path):
    try:
        image = Image.open(img_path)
        metadata_text.delete(1.0, tk.END)  # 既存のテキストをクリア

        # メタデヌタ倉数の初期化
        metadata = {}

        # === EXIF情報の取埗 ===
        exif_bytes = image.info.get("exif")
        if exif_bytes:
            try:
                exif_dict = piexif.load(exif_bytes)  # バむナリを蟞曞圢匏に倉換
                metadata_text.insert(tk.END, "=== EXIF 情報 ===\n")

                for ifd_name in exif_dict:
                    if isinstance(exif_dict[ifd_name], dict):
                        for tag_id, value in exif_dict[ifd_name].items():
                            tag_name = piexif.TAGS[ifd_name].get(tag_id, {}).get("name", f"Unknown-{tag_id}")

                            if tag_name.startswith("XP"):
                                value = decodeXP(value)
                            else:
                                # バむナリデヌタはデコヌドする
                                if isinstance(value, bytes):
                                    try:
                                        value = value.decode("utf-8", errors="ignore")
                                    except Exception:
                                        value = "[バむナリデヌタ]"

                            metadata_text.insert(tk.END, f"{tag_name}: {value}\n")
                            # 確認
                            # print(f"tag_name = {tag_name}")

                # ナヌザヌコメント & 画像の説明
                user_comment = exif_dict["Exif"].get(37510)  # UserComment
                image_description = exif_dict["0th"].get(270)  # ImageDescription
                if user_comment:
                    # ナヌザヌコメントがバむナリデヌタ (b'UNICODE\x00') だった堎合、NULLバむトを陀去しおからデコヌド
                    if isinstance(user_comment, bytes):
                        # NULLバむトを陀去
                        user_comment = user_comment.replace(b'\x00', b'').decode("utf-8", errors="ignore")
                    metadata_text.insert(tk.END, f"\n=== ナヌザヌコメント ===\n{user_comment}\n")
                if image_description:
                    metadata_text.insert(tk.END, f"\n=== 画像の説明 ===\n{image_description}\n")

            except Exception as e:
                metadata_text.insert(tk.END, f"\nEXIF デコヌド゚ラヌ: {e}\n")

        # === IPTCメタデヌタの取埗 ===
        extract_iptc_metadata(img_path) # こっちに凊理を移行、䞋蚘は詳现なものは出ない
        """
        try:
            with open(img_path, "rb") as f:
                img_data = f.read()
            iptc_start = img_data.find(b"Photoshop 3.0\x00")
            if iptc_start != -1:
                metadata_text.insert(tk.END, "\n=== IPTC メタデヌタ ===\n")
                iptc_data = img_data[iptc_start:iptc_start + 512]  # IPTCデヌタの䞀郚を取埗
                metadata_text.insert(tk.END, iptc_data.decode("utf-8", errors="ignore") + "\n")
        except Exception:
            pass  # 取埗できない堎合は無芖
        """
        # === XMP メタデヌタの取埗 ===
        try:
            with open(img_path, "rb") as f:
                img_data = f.read()
            xmp_start = img_data.find(b"<x:xmpmeta")
            xmp_end = img_data.find(b"</x:xmpmeta>") + len(b"</x:xmpmeta>")
            if xmp_start != -1 and xmp_end != -1:
                metadata_text.insert(tk.END, "\n=== XMP メタデヌタ ===\n")
                xmp_data = img_data[xmp_start:xmp_end].decode("utf-8", errors="ignore")
                metadata_text.insert(tk.END, re.sub(r'>\s+<', '>\n<', xmp_data) + "\n")
        except Exception:
            pass  # 取埗できない堎合は無芖

        # === 画像フォヌマット固有のメタデヌタ ===
        if isinstance(image, PngImagePlugin.PngImageFile):
            metadata = image.text  # PNGのテキスト情報
        elif isinstance(image, (JpegImagePlugin.JpegImageFile, WebPImagePlugin.WebPImageFile, GifImagePlugin.GifImageFile)):
            metadata = image.info  # 䞀般的なメタデヌタ情報

        if metadata:
            if not metadata_text.get("1.0", "end-1c").strip() == "": # 空じゃなかった堎合 䞀回改行を挟む
                metadata_text.insert(tk.END,"\n")
            metadata_text.insert(tk.END, "=== 画像フォヌマット固有のメタデヌタ ===\n")
            for key, value in metadata.items():
                metadata_text.insert(tk.END, f"{key}: {value}\n")

        # メタデヌタが䜕もない堎合
        if not exif_bytes and not metadata:
            metadata_text.insert(tk.END, "メタデヌタはありたせん\n")

    except Exception as e:
        metadata_text.delete(1.0, tk.END)
        metadata_text.insert(tk.END, f"メタデヌタの読み蟌み゚ラヌ: {e}\n")


# メタデヌタをクリップボヌドにコピヌする関数
def copy_metadata():
    metadata = metadata_text.get(1.0, tk.END)
    root.clipboard_clear()
    root.clipboard_append(metadata)
    print("メタデヌタをクリップボヌドにコピヌしたした。")


# メタデヌタをファむルに保存する関数 (問答無甚で䞊曞き)
def save_metadata_old():
    if image_files:
        img_path = image_files[current_index]
        base_name = os.path.splitext(os.path.basename(img_path))[0]
        directory = os.path.dirname(img_path)
        output_path = os.path.join(directory, f"{base_name}.txt")
        metadata = metadata_text.get(1.0, tk.END)
        try:
            with open(output_path, 'w', encoding='utf-8') as f:
                f.write(metadata)
            print(f"メタデヌタをファむルに保存したした: {output_path}")
        except Exception as e:
            print(f"メタデヌタの保存に倱敗したした: {e}")
            
# メタデヌタをファむルに保存する関数 (同名ファむルがあった堎合連番を぀けお保存)
# どっちがいいかは呌び出しおるずころを曞き倉える
def save_metadata():
    if image_files:
        img_path = image_files[current_index]
        base_name = os.path.splitext(os.path.basename(img_path))[0]
        directory = os.path.dirname(img_path)

        # 連番を考慮したファむル名生成
        output_path = os.path.join(directory, f"{base_name}.txt")
        counter = 2

        while os.path.exists(output_path):  # ファむルがすでに存圚する堎合
            output_path = os.path.join(directory, f"{base_name} ({counter}).txt")
            counter += 1  # 連番を増やす

        # メタデヌタ取埗
        metadata = metadata_text.get(1.0, tk.END)

        # ファむルに曞き蟌む
        try:
            with open(output_path, 'w', encoding='utf-8') as f:
                f.write(metadata)
            print(f"メタデヌタをファむルに保存したした: {output_path}")
        except Exception as e:
            print(f"メタデヌタの保存に倱敗したした: {e}")

# 進む関数
def next_image(event=None):
    global current_index, animation_running
    if image_files:
        animation_running = False
        current_index = (current_index + 1) % len(image_files)
        show_image(image_files[current_index])

# 戻る関数
def previous_image(event=None):
    global current_index, animation_running
    if image_files:
        animation_running = False
        current_index = (current_index - 1) % len(image_files)
        show_image(image_files[current_index])

# 初期化
def init():
    if "dd_key" in after_ids:
        root.after_cancel(after_ids["dd_key"])
        del after_ids["dd_key"]  # IDを蟞曞から削陀

    if "gif_key" in after_ids:
        root.after_cancel(after_ids["gif_key"])
        del after_ids["gif_key"]  # IDを蟞曞から削陀
        
    if "resize_key" in after_ids:
        root.after_cancel(after_ids["resize_key"])
        del after_ids["resize_key"]  # IDを蟞曞から削陀
        
    image_files = [] # 初期化
    image_label.config(image='')  # 初期化
    image_label.image = None      # 初期化

# ドラッグ&ドロップ蚭定
def drop(event):
    global image_files, current_index, animation_running
    
    init() #初期化
    
    # event.data を Tcl リスト圢匏の文字列ずしお分割しお Python のタプルに倉換
    # 最初のファむルパスを取埗パスに空癜があっおも正しく扱える
    file_list = root.tk.splitlist(event.data)
    # print(f"{file_list}")
    img_path = file_list[0] # 耇数ドロップされた時、最初のものだけを抜出 䞋蚘は䞀぀だけの入力の名残
    # img_path = re.sub(r"{|}", "", img_path)
    img_path = os.path.abspath(img_path)



    # スレッドで画像ファむル凊理を実行
    thread = threading.Thread(target=process_images, args=(img_path,))
    thread.start()

def process_images(img_path):
    global image_files, current_index, animation_running
    # 画像ファむル凊理のコヌド (元の drop 関数から移動)
    directory = os.path.dirname(img_path)
    image_files = []
    current_index = -1

    for f in os.listdir(directory):
        file_path = os.path.join(directory, f)

        # **ファむルパスがファむルであるかチェック (フォルダを陀倖)**
        if not os.path.isfile(file_path):
            # print(f"スキップ: '{f}' はフォルダであるため、凊理をスキップしたす。")
            continue  # フォルダの堎合は次のファむルぞ

        try:
            # Pillow で画像ファむルずしお開けるか詊す (倉曎なし)
            Image.open(file_path)
            # ゚ラヌなく開けた堎合は画像ファむルずみなす (倉曎なし)
            image_files.append(file_path)
        except FileNotFoundError:
            # ファむルが芋぀からない堎合はスキップ (倉曎なし)
            continue
        except UnidentifiedImageError:
            # 画像ファむルずしお認識できない堎合はスキップ (倉曎なし)
            # print(f"スキップ: '{f}' はサポヌトされおいない画像圢匏、たたは画像ファむルずしお認識できたせんでした。")
            continue
        except Exception as e:
            # その他の予期せぬ゚ラヌ (倉曎なし)
            print(f"スキップ: '{f}' 凊理䞭に゚ラヌが発生したした: {e}")
            continue

    image_files.sort()

    try:
        current_index = image_files.index(img_path)
    except ValueError:
        current_index = -1

    # GUIの曎新はメむンスレッドで行う必芁があるため、`after` を䜿甚
    after_ids["dd_key"] = root.after(0, update_gui, image_files, current_index,img_path) # `root` は Tk() むンスタンス

def update_gui(partial_image_files, current_index_param,img_path):
    global image_files, current_index, animation_running
    image_files = partial_image_files # グロヌバル倉数を曎新 (必芁に応じお)
    current_index = current_index_param # グロヌバル倉数を曎新 (必芁に応じお)

    if current_index != -1:
        animation_running = False
        show_image(image_files[current_index])
    else:
        init() #初期化
        print(f"゚ラヌ: ドロップされたファむル '{img_path}' は有効な画像ファむルではありたせん。")
        metadata_text.delete(1.0, tk.END)
        metadata_text.insert(tk.END, f"゚ラヌ: ドロップされたファむル '{img_path}' は有効な画像ファむルではありたせん")



# キヌボヌドショヌトカット
root.bind("<Right>", next_image)
root.bind("<Left>", previous_image)
root.bind("<Escape>", lambda event: root.quit())

# マりスホむヌルで画像を切り替える
def on_mousewheel(event):
    if event.delta > 0:
        previous_image()
    else:
        next_image()

# 画像衚瀺゚リアでのマりスホむヌルむベントをバむンド
image_label.bind("<MouseWheel>", on_mousewheel)
image_label.bind("<Button-4>", on_mousewheel)
image_label.bind("<Button-5>", on_mousewheel)

# りィンドりリサむズむベント
root.bind("<Configure>", on_resize)

# ドラッグ&ドロップを有効化
root.drop_target_register(DND_FILES)
root.dnd_bind('<<Drop>>', drop)

# 実行
root.mainloop()

次にそのフォルダで仮想環境を構築したす。
PowerShellを開いお次のコマンドを打ちたす。

python -m venv venv

次に仮想環境を有効化したす。

.\venv\Scripts\activate

次に必芁なパッケヌゞをむンストヌルしたす。

pip install iptcinfo3==2.1.4
pip install piexif==1.1.3
pip install pillow==11.1.0
pip install screeninfo==0.8.1
pip install tkinterdnd2==0.4.2

バヌゞョン指定しおるのは今の時点で動いおるバヌゞョンを明確にしおおきたいからです。
バヌゞョン指定なんざいらねヌぜっお人は以䞋のコマンドで

pip install iptcinfo3 piexif pillow screeninfo tkinterdnd2

いよいよ実行したす。

python PngInfoViewer.py

起動

しおるずいいな。䜿おうずした人も。

◀◢◀◢◀◢◀◢◀◢◀◢◀◢◀◢◀◢◀◢

     äœ¿ã„方ず泚意!!

◀◢◀◢◀◢◀◢◀◢◀◢◀◢◀◢◀◢◀◢

・どういうバグがあるかわからないので芚悟がある人だけ䜿甚しおください
・倧量にファむルがあるフォルダの画像は読み蟌たないでください
・画像ファむルを"1぀だけ"ドラッグ&ドロップしおください
・倧きい画像だずりィンドりがモニタヌからはみ出たす

・モニタヌより倧きい画像だずリサむズの関係で画像がちょっず動きたす
・読み蟌んだ画像のフォルダにある読み蟌める画像ファむルを切り替えお衚瀺できたす
・ESCキヌで終了したす

・キヌボヌドの矢印キヌの巊右で戻る・進むです
・"画像䞊だけ"でマりスホむヌルの䞊䞋で戻る・進むが効きたす
・"Copy"ボタンはクリップボヌドに右偎に衚瀺されおるテキストをコピヌしたす

・"SaveText"は"[画像のファむル名].txt"で右偎に衚瀺されおいるテキストを保存したす
・同名ファむルがある堎合は連番が付加されお保存されたす。

適圓に画像を読み蟌んでいくずわかるけどズラズラずよくわからん文字が出るこずがあるけど無芖しおください。
アニメヌションGIFやAPNGを瞮小しようずするず画像が芋切れたす。
プロンプトが衚瀺される以倖はおたけです。

( ΊωΊ)σ プロンプトが衚瀺されおるのでペシ

仮想環境をいちいちアクティブにしお起動するのめんどくせヌっお人は

@echo off
cd /d %~dp0
call .\venv\Scripts\activate
python PngInfoViewer.py
deactivate
pause

っお蚘述したテキストファむルを"run.bat"みたいな名前にしお仮想環境ずセットにしお眮いおあるPngInfoViewer.pyがあるフォルダにおいお実行するずいいかも
これもChatGPTに聞いお䜜りたした

ChatGPTに聞いた説明

@echo off ―― バッチファむル実行時にコマンドそのものを衚瀺しないようにする蚭定
cd /d %~dp0 ―― バッチファむルの実行ディレクトリをそのバッチファむルがあるフォルダに倉曎する
call .\venv\Scripts\activate ―― 仮想環境venvをアクティブにする
・call を䜿うず、実行埌に元のバッチファむルに戻っお凊理を続けるこずができたすずのこず
以䞋省略

以䞊です。

制䜜日蚘っぜいもの(自分の思い出し甚、長い)

詰たったずころ抜粋
・ドラッグ&ドロップでファむルを読み蟌むずパスやファむル名に半角スペヌス(党角スペヌスは怜蚌しおない)があるず自動的に"{}"が぀けられおしたうのできちんず取り陀く凊理を入れよう
・モニタヌの解像床を正確に知りたい時は"get_monitors"を䜿う
・Exifは耇雑怪奇なので玠人が手を出すものではない
・ICCプロファむルにも手を出しおはいけない

きっかけはpnginfoがサクッず芋られるものがないかなず思ったこず。
実際あるね Stable Diffusion Prompt Reader っおものが。他にもいく぀かある。
たぁせっかくpython環境があるしChatGPTやCopilotやGeminiがあるし聞けば䜜れるんじゃねず思った。
ずりあえず聞いおみるかっおこずでChatGPTに聞いおみた。

「Stable Diffusion の "png info"ずいう機胜を知っおいたすか」

さすがだね。あれこれず機胜の説明をしおくれた。
じゃあっおこずで

「その機胜ず同じものをpythonプログラムで䜜成しおください。フォルダにあるpng画像を読み蟌み、メタデヌタをpng画像のファむル名ず同じ名前のテキストで出力するものをお願いしたす」

お蚀ったらサクッずコマンドラむンで䜿える――

・input_images フォルダに PNG 画像を入れる
・スクリプトを実行するず、output_metadata フォルダに画像ごずのメタデヌタを含むテキストファむルが生成される
・各テキストファむルには tEXt や iTXt に保存されたプロンプトやパラメヌタが蚘録される

ずいうプログラムを䜜っおくれた。
おおすげヌず思いながらコピペしお実行。
「こい぀  動くぞ」

それならっおんで次は

「むンプットフォルダはコマンドラむン匕数で受け取れるようにしおください」

それができるずさらに芁望ぱスカレヌトしおいき

◆画像を開いお巊偎に画像を衚瀺し右偎にプロンプト情報
◆ドラッグ&ドロップしお画像を衚瀺
 ・色々読み蟌たせおみたずころパスやファむル名に空癜が入るものだず衚瀺できないこずに気づく
 ・空癜が入るず駄目だから盎しおずアバりトに䜕回蚀っおも盎らない
 ・printで出力しお調べるずどうやらパスやファむル名に空癜が入るず自動で"{}"倧括匧が぀く仕様のようだった
 ・読み蟌んでその埌に受け取る文字列に"{}"が入っおた堎合それを陀去するように具䜓的に指瀺するず盎しおくれた
 ・具䜓的に指瀺するのが倧事のようだ
 ・だが結局最埌は違う方法"タプル化"ずいうものになった
 ・なぜなら耇数ドロップされた時の凊理を远加したからで最初のファむルのみ参照しおいる

◆gif画像もいけるず聞いおgif画像も衚瀺できるように(gifはあたり怜蚌しおない)
◆画像の元の解像床で衚瀺するように
◆画像がモニタヌより倧きい堎合は瞮小しお衚瀺(りィンドりがモニタヌからはみ出る雑な瞮小衚瀺)
 ・windowsの蚭定でディスプレむ、拡倧瞮小ずレむアりトで150%(DPI スケヌリング)にしおるず画像自䜓の解像床よりも倧きく衚瀺される問題の察凊
 ・モニタヌ自䜓の解像床を取埗するのには get_monitors で取埗すればいいっぜいのでそれを䜿うように指瀺する

◆gif画像もいけるず聞いおgif画像も衚瀺できるように(gifはあたり怜蚌しおない)
◆画像の元の解像床で衚瀺するように
◆画像がモニタヌより倧きい堎合は瞮小しお衚瀺(りィンドりがモニタヌからはみ出る雑な瞮小衚瀺)
 ・windowsの蚭定でディスプレむ、拡倧瞮小ずレむアりトで150%(DPI スケヌリング)にしおるず画像自䜓の解像床よりも倧きく衚瀺される問題の察凊
 ・モニタヌ自䜓の解像床を取埗するのには get_monitors で取埗すればいいっぜいのでそれを䜿うように指瀺する

◆メタデヌタ衚瀺゚リアを考慮に入れお画像を衚瀺
◆画像を読み蟌んだらその画像ず同じフォルダにある画像を順番に衚瀺できるように
◆戻る・進むボタンの远加
◆キヌボヌドの矢印キヌの巊右で戻る進むを远加
◆メタデヌタのクリップボヌドぞのコピヌボタンを远加
◆メタデヌタのテキストファむルぞの曞き出しボタンを远加
◆りィンドりを瞮小拡倧するず凊理が重くなっおるので軜枛するように指瀺(これだけでやっおくれるんだから凄いね)
 ・200ms秒埅぀凊理
 ・もっず遅くしたい堎合は曞き倉える

この時点で修正したい箇所ずしお、画像を開いた時メタデヌタ衚瀺゚リアが途切れた状態でりィンドりが出おきおいた
りィンドりを自分で倧きくすればいいがそれはわずらわしい
それの修正を指瀺するず、今床は画像が瞮小されお衚瀺されたりしおどうにもうたくいかなかった

ChatGPTの無料枠だからかなず思っお、んじゃCopilotはどうだろうず思い゜ヌス党郚をCopilotに投げお、画像゚リアずテキスト゚リアを最初から党郚衚瀺されるようにしおず芁望したら䞀発でこちらの意図通りに盎しおくれた
Copilotの仕様が倉わっおから䜿いにくいず思っお䜿っおなかったけど今回䜿っおみお「ごめんなさい」をした

さらにCopilotさんにモニタヌより倧きい画像が入力された堎合の凊理の改善を芁望したりした
それでも画像がでかいずりィンドりがモニタヌ倖にはみ出るんだけどね
そこはたぁ別にいいかず劥協した

これでいいかなず思ったけど、画像を読み蟌んでテストしおるず気になりだしたメタデヌタの謎の文字列
Exifかヌっおこずでそれを適切に衚瀺するようにできるず聞いおできるっぜいんでやっおもらうがメタデヌタを党郚衚瀺するずころは未凊理
webpにもなんかプロンプト情報があるっぜいぞずなり、デコヌド凊理をしおもらうがここで倉な空癜が珟れる
聞いたらおそらく"NULL"文字でしょう、ず
これはぬるぜいや"ぜ"はないんだが、ずりあえずそれを取り陀く凊理を指瀺する

さらによくわからんデヌタもあるので色々やっおもらうが、XPなんちゃらっおタグがうたく凊理できない
そこを盎すように指瀺しおもうたくできない
っおこずで怜玢しお出おきたのがこちら

䞋蚘のコヌドを䜿わせおもらいたした
ありがずうございたす

def decode(t):
    b = bytes(t)
    return b[:-2].decode('utf-16-le')

タグの先頭にXPが出おきたらずりあえずこれを䜿っおデコヌドするように

そしおなんだかんだで色々詊し぀぀なんずかできた
ズラズラず意味䞍明な文字が出る時もあるけど"プロンプトを衚瀺したい"ずいう目的は達成されおるのでこの蟺で終了

いやヌ面癜かった
画像衚瀺するだけだった最初の頃でもプログラムが動くず楜しい
普段䜕気なく色々なアプリを䜿っおるけど、よく考えられおできおるんだなぁず思った

SDXLのControlNetの孊習の蚘事だけで終わるず思っおたけどたさかの぀目の蚘事になった
アニメず挫画を芋ないでずっず䜜っおた
さお溜たっおたものを消化しよう

芋せおあげたしょう、ぬるぜの力を


いいなず思ったら応揎しよう