『第十六文書写像術』は、任意のテキストを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()