テキストファイル画像コード化ツール『第十六文書写像術』』 ソースコード

『第十六文書写像術』は、任意のテキストを16色のカラーパレットでエンコードし、可逆的に画像として保存・復元できるツールです。

>>GUI外観 >>使い方

以下は『第十六文書写像術』のソースコード全文です:

 
        import tkinter as tk
        from tkinter import filedialog, messagebox
        from PIL import Image, ImageTk
        import math
        import datetime
        import os
        
        COLOR_PALETTE = [
            (0, 0, 0), (128, 0, 0), (0, 128, 0), (128, 128, 0),
            (0, 0, 128), (128, 0, 128), (0, 128, 128), (192, 192, 192),
            (128, 128, 128), (255, 0, 0), (0, 255, 0), (255, 255, 0),
            (0, 0, 255), (255, 0, 255), (0, 255, 255), (255, 255, 255)
        ]
        
        class ColorBarcodeApp:
            def __init__(self, root):
                # ----- 基本設定・変数初期化 -----
                self.root = root
                self.root.title("テキストファイル画像コード化ツール - 第十六文書写像術")
                self.mode = tk.StringVar(value="encode")
                self.cell_size = 20  # 1セルのピクセルサイズ
                self.image_paths = []  # デコード用画像パスのリスト
        
                # ----- モード表示ラベル -----
                self.mode_label = tk.Label(root, text="", anchor="e")
                self.mode_label.pack(anchor="ne", padx=10, pady=5)
        
                # ----- メニューバー構築 -----
                menubar = tk.Menu(root)
                mode_menu = tk.Menu(menubar, tearoff=0)
                mode_menu.add_command(label="画像化", command=self.switch_to_encode)
                mode_menu.add_command(label="テキスト化", command=self.switch_to_decode)
                menubar.add_cascade(label="モード", menu=mode_menu)
                root.config(menu=menubar)
        
                # ========================================
                # 画像化(エンコード)モード UI 構築
                # ========================================
        
                # 入力テキスト欄
                self.text = tk.Text(root, height=20)
        
                # 解像度選択ドロップダウン
                self.resolution_var = tk.StringVar(value="640x360")
                self.resolution_menu = tk.OptionMenu(root, self.resolution_var, "640x360", "960x540", "1280x720")
                self.resolution_menu.pack(pady=2)
        
                # プレビュー・保存ボタン
                self.preview_button = tk.Button(root, text="プレビュー", command=self.preview_encode)
                self.save_button = tk.Button(root, text="保存/分割保存", command=self.save_images)
        
                # プレビュー画像の表示エリア
                self.image_label = tk.Label(root)
        
                # ========================================
                # テキスト化(デコード)モード UI 構築
                # ========================================
        
                # 画像選択ボタンとパス表示
                self.select_button = tk.Button(root, text="画像を選択", command=self.select_image)
                self.image_path_label = tk.Label(root, text="", fg="blue")
        
                # プレビュー・変換ボタン
                self.decode_preview_button = tk.Button(root, text="プレビュー", command=self.preview_decode)
                self.decode_button = tk.Button(root, text="変換", command=self.decode_image)
        
                # デコード結果表示用フレーム(テキスト+クリアボタン)
                self.decode_frame = tk.Frame(root)
                self.decode_frame.pack_forget()
                self.decoded_text = tk.Text(self.decode_frame, height=20)
                self.clear_button = tk.Button(self.decode_frame, text="クリア", command=self.clear_decoded_text)
        
                self.decoded_text.pack(side="left", fill="both", expand=True)
                self.clear_button.pack(side="right", padx=5, pady=5)
        
                # 書き出しボタン(デコード結果を保存)
                self.export_button = tk.Button(root, text="書き出し", command=self.export_text)
        
                # ========================================
                # デコード:複数画像リスト UI
                # ========================================
        
                self.list_frame = tk.Frame(root)
        
                # リストボックスとスクロールバー
                self.image_listbox = tk.Listbox(self.list_frame, height=5)
                self.scrollbar = tk.Scrollbar(self.list_frame, orient="vertical", command=self.image_listbox.yview)
                self.image_listbox.config(yscrollcommand=self.scrollbar.set)
        
                self.image_listbox.grid(row=0, column=0, sticky="nsew")
                self.scrollbar.grid(row=0, column=1, sticky="ns")
                self.list_frame.grid_columnconfigure(0, weight=1)
        
                # 選択画像をリストから削除するボタン
                self.remove_button = tk.Button(root, text="選択画像をリストから削除", command=self.remove_selected_image)
        
                # ========================================
                # 初期モードをエンコードに設定
                # ========================================
                self.switch_to_encode()
        
            def clear_widgets(self):
                for widget in self.root.winfo_children():
                    if not isinstance(widget, tk.Menu) and widget != self.mode_label:
                        widget.pack_forget()
                self.decode_frame.pack_forget()
        
            def switch_to_encode(self):
                self.clear_widgets()
                self.mode.set("encode")
                self.text.pack(fill='x', padx=10, pady=5)
                self.resolution_menu.pack(pady=2)
                self.preview_button.pack(pady=2)
                self.save_button.pack(pady=2)
                self.image_label.pack(padx=10, pady=10)
                self.show_image(None)
                self.mode_label.config(text="mode:画像化(encode)モード")
            
            def switch_to_decode(self):
                self.clear_widgets()
                self.mode.set("decode")
                self.select_button.pack(pady=5)
                self.list_frame.pack(padx=10, pady=2, fill='x')
                self.remove_button.pack(pady=2)
                self.decode_preview_button.pack(pady=2)
                self.decode_button.pack(pady=2)
                self.image_label.pack(padx=10, pady=10)
                self.show_image(None)
                self.decode_frame.pack(fill='x', padx=10, pady=5)
                self.export_button.pack(pady=5)
                self.mode_label.config(text="mode:文書化(decode)モード")
        
            def preview_encode(self):
                data = self.text.get("1.0", tk.END).strip()
                if not data:
                    messagebox.showwarning("警告", "文字列を入力してください。")
                    return
                try:
                    images = self.encode_text_to_images(data)
                    if images:
                        self.image = images[0]
                        self.show_image(self.image)
                except Exception as e:
                    messagebox.showerror("エンコードエラー", str(e))
                 
            def save_images(self):
                data = self.text.get("1.0", tk.END).strip()
                if not data:
                    messagebox.showwarning("警告", "文字列を入力してください。")
                    return
            
                try:
                    # データを画像群に変換
                    images = self.encode_text_to_images(data)
                    if not images:
                        messagebox.showerror("エラー", "画像の生成に失敗しました。")
                        return
            
                    # ファイル保存ダイアログ
                    filetypes = [("PNG files", "*.png"), ("BMP files", "*.bmp"), ("GIF files", "*.gif")]
            
                    filepath = filedialog.asksaveasfilename(
                        defaultextension=".png",
                        initialfile="barcode",
                        filetypes=filetypes,
                        title="保存ファイル名を指定"
                    )
                    if not filepath:
                        return
            
                    # 拡張子とベース名を分離
                    base, ext = os.path.splitext(filepath)
                    ext = ext.lower().replace(".", "")
                    if ext not in ("png", "bmp", "gif"):
                        ext = "png"
            
                    now = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
            
                    for i, img in enumerate(images, start=1):
                        seq = f"{i:02d}"
                        filename = f"{base}-{now}_{seq}.{ext}"
                        img.save(filename)
            
                    messagebox.showinfo("保存完了", f"{len(images)} 枚の画像を保存しました。")
                except Exception as e:
                    messagebox.showerror("保存エラー", str(e))
        
            def select_image(self):
                filepaths = filedialog.askopenfilenames(
                    filetypes=[
                        ("画像ファイル", "*.bmp *.png *.gif"),
                        ("BMP files", "*.bmp"),
                        ("PNG files", "*.png"),
                        ("GIF files", "*.gif"),
                    ]
                )
                for filepath in filepaths:
                    if filepath not in self.image_paths:
                        self.image_paths.append(filepath)
                        self.image_listbox.insert(tk.END, filepath)
        
            def preview_decode(self):
                if not self.image_paths:
                    messagebox.showwarning("警告", "画像が選択されていません。")
                    return
                try:
                    img = Image.open(self.image_paths[0]).convert("RGB")
                    self.image = img
                    self.show_image(self.image)
                except Exception as e:
                    messagebox.showerror("プレビューエラー", str(e))
        
            def remove_selected_image(self):
                selected_indices = self.image_listbox.curselection()
                if not selected_indices:
                    messagebox.showinfo("情報", "削除する画像を選択してください。")
                    return
                for index in reversed(selected_indices):
                    self.image_listbox.delete(index)
                    del self.image_paths[index]
        
            def decode_image(self):
                if not self.image_paths:
                    messagebox.showwarning("警告", "画像が選択されていません。")
                    return
            
                hex_digits = []
                errors = []
                try:
                    sorted_paths = sorted(self.image_paths)  # 並び順を安定化
                    for path in sorted_paths:
                        try:
                            img = Image.open(path).convert("RGB")
                            pixels = img.load()
                            width, height = img.size
            
                            def closest_color(c):
                                return min(range(16), key=lambda i: sum((a - b) ** 2 for a, b in zip(c, COLOR_PALETTE[i])))
            
                            img_digits = [closest_color(pixels[x, y]) for y in range(height) for x in range(width)]
            
                            if len(img_digits) < 6:
                                raise ValueError(f"{os.path.basename(path)}: メタ情報不足")
            
                            length = (
                                (img_digits[0] << 20) |
                                (img_digits[1] << 16) |
                                (img_digits[2] << 12) |
                                (img_digits[3] << 8) |
                                (img_digits[4] << 4) |
                                img_digits[5]
                            )
            
                            if length > len(img_digits) - 6:
                                raise ValueError(f"{os.path.basename(path)}: データ長が過剰です")
            
                            data_digits = img_digits[6:6 + length]
                            hex_digits.extend(data_digits)
            
                        except Exception as e:
                            errors.append(f"{os.path.basename(path)}: {str(e)}")
            
                    if len(hex_digits) % 2 != 0:
                        hex_digits.append(0)
            
                    bytes_out = bytearray((hex_digits[i] << 4) | hex_digits[i + 1] for i in range(0, len(hex_digits), 2))
                    text = bytes_out.decode("utf-8")
            
                    self.decoded_text.delete("1.0", tk.END)
                    self.decoded_text.insert(tk.END, text)
            
                    if errors:
                        messagebox.showwarning("一部失敗", "\n".join(errors))
            
                except Exception as e:
                    messagebox.showerror("デコードエラー", str(e))
        
            def show_image(self, image):
                self.image = image
                if image is None:
                    self.image_label.configure(image="")
                    self.image_label.image = None
                    return
            
                max_width = 320
                max_height = int(max_width * 9 / 16)
                scale = min(max_width / image.width, max_height / image.height, 1.0)
                new_width = int(image.width * scale)
                new_height = int(image.height * scale)
                resized = image.resize((new_width, new_height), Image.NEAREST)
                tk_img = ImageTk.PhotoImage(resized)
                self.image_label.configure(image=tk_img)
                self.image_label.image = tk_img
            
            def encode_text_to_images(self, data):
                byte_data = data.encode("utf-8")
                hex_digits = []
                for byte in byte_data:
                    hex_digits.append(byte >> 4)
                    hex_digits.append(byte & 0x0F)
            
                if len(hex_digits) % 2 != 0:
                    hex_digits.append(0)  # 全体でもバイト境界を保つ
            
                total_hex = len(hex_digits)
                meta_len = 6  # 24bitのデータ長メタ情報
            
                # 解像度取得
                res_str = self.resolution_var.get()
                width, height = map(int, res_str.split("x"))
                max_pixels = width * height
                usable_pixels = max_pixels - meta_len
                max_hex_per_image = usable_pixels
            
                images = []
                for start in range(0, total_hex, max_hex_per_image):
                    part = hex_digits[start:start + max_hex_per_image]
            
                    # バイト単位(偶数)に揃える
                    if len(part) % 2 != 0:
                        part.append(0)
            
                    part_len = len(part)
            
                    # メタ情報の付加
                    meta = [
                        (part_len >> 20) & 0x0F,
                        (part_len >> 16) & 0x0F,
                        (part_len >> 12) & 0x0F,
                        (part_len >> 8) & 0x0F,
                        (part_len >> 4) & 0x0F,
                        part_len & 0x0F,
                    ]
            
                    full_data = meta + part
                    # 残りピクセルは余白として白で埋める
                    pad_length = max_pixels - len(full_data)
                    full_data += [15] * pad_length  # 15: (255,255,255)=白
            
                    img = Image.new("RGB", (width, height), (255, 255, 255))
                    pixels = img.load()
                    for idx, val in enumerate(full_data):
                        x = idx % width
                        y = idx // width
                        pixels[x, y] = COLOR_PALETTE[val]
                    images.append(img)
            
                if images:
                    self.image = images[0]
                    self.show_image(self.image)
            
                return images
            
            def clear_decoded_text(self):
                self.decoded_text.delete("1.0", tk.END)
        
          
            def export_text(self):
                text = self.decoded_text.get("1.0", tk.END).strip()
                if not text:
                    messagebox.showwarning("警告", "書き出すテキストがありません。")
                    return
            
                base_name = os.path.basename(self.image_paths[0]) if self.image_paths else "output"
                base_name = os.path.splitext(base_name)[0]
                now = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
                filename = f"{base_name}-{now}.txt"
            
                file_path = filedialog.asksaveasfilename(
                    defaultextension=".txt",
                    initialfile=filename,
                    filetypes=[("Text files", "*.txt")],
                    title="保存先を指定"
                )
            
                if not file_path:
                    return  # キャンセル時は何もしない
            
                try:
                    with open(file_path, "w", encoding="utf-8") as f:
                        f.write(text)
                    messagebox.showinfo("保存完了", f"{os.path.basename(file_path)} を保存しました。")
                except Exception as e:
                    messagebox.showerror("保存エラー", str(e))
        
        if __name__ == "__main__":
            root = tk.Tk()
            
            ##タイトルバーアイコン表示用
            data = '''R0lGODdhEAAQAIUAAC1IXKq4u26IlTZVapSpsDJNYdPW0UtmeFV1hoSbpHuWomN9jOTk3MXLyP/88kxUcOnq4mByfSE9U7vDwqaJcZWHho52dXuQka++wFSFhh87UaWQenCIf3xqfl9xfZywtk5PcjiCkSFuiyNgfDBcggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAEAAQAEAIuQAXCFCQgACBAAgRGkwgQMCBAQIYJGxgQMAFDhkyHDggMAEDBhAgNDBIgKGABQgKAABwIEKElg8HyBxQoIBABQYTJiypoCGCAwJEGhhq4EKFChY6/BwAYIEBgw0kPiTxAITMlVgB1NxaE+vNnAEwYFBY0udAnB90Gux5EgGCBA7iOmiQoC5btw8jBpgwYaiADRQoWNhI0+nEBm5DiBjx4KoHBkQlznzQmOlKCRo0SJAAgCbXrKBDrwwIADs=''' 
            root.tk.call('wm', 'iconphoto', root._w, tk.PhotoImage(data=data))
            ####
            
            app = ColorBarcodeApp(root)
            root.mainloop()
        
        
       
    

📦 ダウンロード 🏠 ホームへ