開発者用拡張機能を公開するサイト
pythonで画像ビュアーを作る
これはWindows10を使い始めた頃から思っていることではありますが、
デフォルトの画像ビュアーである「フォト」ってすごく使いにくいですよね。
理由は様々あるのですが、私は対処として「フォトビューアー」を有効化して使っていました。
しかしてWindows11に乗り換えた今、フォトビューアーがいつまで使えるかわかりません。
一応レジストリを変更して無理やり起動することはできますが、
WindowsUpdateでいつ消えるかもわかりません。
さてどうするか。
フォトビューアーはフリーツールを探せば色々あります。
ただ個人的には搭載されている機能など画像を確認できればそれでいいのです。
編集、印刷といった機能は他のツールを使います。
であるならば。
プログラマであるなら作ってしまってもいいではないか。
という研究の意も込めて自作することにしました。
使用言語はpython。
採用理由:visual studio使えばC#で作れるけどpythonで作ってもいいじゃない。
330行あります。
なお精査しておりませんのでグチャグチャだったりクドイ記述があります。
またご利用は自己責任で。

動作画面
フォトビューアーにあってフォトでなくなった機能を搭載しています。
まず個人的にフォトで使いにくくなった点として
特に困ったのはループとマウスの4,5ボタン。
ループはどうしようもないとして、
マウスだけで画像送りをしたければ一応画面隅に移動ボタンがあります。
が、なぜフォトビューアーでできた機能を削ったのかと。
なおフォトで4,5ボタンを使うと概ね左クリックと同じ挙動をします。
あと画像送り後の調整は画像の位置が変だったり拡大されていたりで非常に見辛い。
あくまで個人用に作ったのでGUI面は放棄しています。
フォトビューアーでも原寸拡大ボタンぐらいしか使わなかったので。
4,5ボタンはボタン番号でしか検出できなかったので「進む/戻る」に対応しているかは不明です。
自己署名証明書を作成して署名もしておいたのですが、
その辺りの設定はこちらのページで非常にわかりやすく書かれていたのでリンクを掲載しておきます。
【PowerShell】自作の.exeファイルを自己署名を入れる方法 / しぐにゃもブログ
このビューアーは画像を表示する時にPillowが一度メモリへ展開します。
つまり画像が大きければ大きいほどメモリを消費するということです。
初期ウインドウサイズであればだいたい20~40MBほどですが
6000pxぐらいの画像を原寸表示すると400MBぐらいは消費し始めます。
photoImageのzoomメソッドを使えばどうかと思ったのですが
pillowのImageTkで作ったphotoImageにはzoomメソッドが入ってないらしい。
canvasの見た目だけ拡大できるような機能があればいいのですけどね。
またCMYK画像を表示するにはadobeが公開しているiccを用意する必要があります。
exeと同フォルダに置いておけば読み込まれます。
iccファイルがない状態だと「この画像は表示できません」エラーになります。
そして表示できても多少暗い画像になります。
デフォルトの画像ビュアーである「フォト」ってすごく使いにくいですよね。
理由は様々あるのですが、私は対処として「フォトビューアー」を有効化して使っていました。
しかしてWindows11に乗り換えた今、フォトビューアーがいつまで使えるかわかりません。
一応レジストリを変更して無理やり起動することはできますが、
WindowsUpdateでいつ消えるかもわかりません。
さてどうするか。
フォトビューアーはフリーツールを探せば色々あります。
ただ個人的には搭載されている機能など画像を確認できればそれでいいのです。
編集、印刷といった機能は他のツールを使います。
であるならば。
プログラマであるなら作ってしまってもいいではないか。
という研究の意も込めて自作することにしました。
使用言語はpython。
採用理由:visual studio使えばC#で作れるけどpythonで作ってもいいじゃない。
まずコード全文
機密もなにもないので全文公開します。330行あります。
なお精査しておりませんのでグチャグチャだったりクドイ記述があります。
またご利用は自己責任で。
from PIL import Image, ImageTk, ImageCms, ImageDraw, ImageFont
import tkinter
import io
import os,sys,glob,mimetypes
class pyImageViewer:
def __init__(self):
sep_dir = sys.argv[1].split(os.sep)
self.img_name = sep_dir.pop()
sep_dir = '{}{}'.format((os.sep).join(sep_dir), os.sep)
file_list = glob.glob('{}*.*'.format(sep_dir))
self.file_list2 = []
exte_list = ['.raw']
for img_path in file_list:
if (
(mimetypes.guess_type(img_path)[0] is not None) and
('image' in mimetypes.guess_type(img_path)[0]) and
((os.path.splitext(img_path)[1].lower() in exte_list) == False)
):
self.file_list2.append(img_path)
self.file_len = len(self.file_list2)
self.file_index = self.file_list2.index(sys.argv[1])
#=======================
self.size_list = {'mw':960,'mh':540,'iw':0,'ih':0,'sw':1,'sh':1,'f':10,'z':0,'fit':True,'fit_z':0,}
self.size_list['nw'] = self.size_list['mw']
self.size_list['nh'] = self.size_list['mh'] - int(self.size_list['f'] * 2.5)
self.size_list['pw'] = int(self.size_list['nw'] / 2)
self.size_list['ph'] = int(self.size_list['nh'] / 2)
self.def_image = None
self.set_image = None
self.canvas_image = None
self.ImageViewer= tkinter.Tk()
self.ImageViewer.title("PythonImageViewer")
self.ImageViewer.geometry("{}x{}".format(self.size_list['mw'],self.size_list['mh']))
self.ImageViewer.bind("<KeyPress>", self.key_down)
self.ImageViewer.bind("<Configure>", self.on_resize)
self.ImageViewer.bind("<ButtonPress>", self.mouse_button)
self.ImageViewer.bind("<MouseWheel>", self.mouse_wheel)
self.ImageViewer.bind("<Motion>", self.mouse_move)
#=======================
self.canvas = tkinter.Canvas(self.ImageViewer, width=self.size_list['nw'], height=self.size_list['nh'])
self.canvas.pack()
self.info_label = tkinter.Label(font=("System",self.size_list['f']),anchor="w")
self.info_label.pack(fill="x",ipadx=2)
self.image_set()
self.ImageViewer.mainloop()
#=======================
#キーボード
def key_down(self, event):
if event.keysym == 'Right':
self.size_list['fit'] = True
self.file_index_change(0)
elif event.keysym == 'Left':
self.size_list['fit'] = True
self.file_index_change(1)
else:
return False
self.size_list['fit_z'] = 0
self.image_center()
self.image_set()
#=======================
#マウス
def mouse_button(self, event):
if event.num == 5:
self.size_list['fit'] = True
self.file_index_change(0)
elif event.num == 4:
self.size_list['fit'] = True
self.file_index_change(1)
elif event.num == 1:
if self.size_list['fit_z'] == self.size_list['z']:
self.size_list['fit'] = False
self.size_list['z'] = 100
else:
self.size_list['fit'] = True
self.size_list['z'] = self.size_list['fit_z']
else:
return False
self.size_list['fit_z'] = 0
self.image_center()
self.image_set()
#=======================
#マウスホイール
def mouse_wheel(self, event):
zoom_set = 10
self.size_list['fit'] = False
if 0 < event.delta:
#上回転
if self.size_list['z'] == 100:
return False
else:
if self.size_list['z'] == 10:
return False
zoom_set = -10
self.size_list['z'] = (int(self.size_list['z'] / 10) * 10) + zoom_set
if self.size_list['z'] < 10:
self.size_list['z'] = 10
elif 100 < self.size_list['z']:
self.size_list['z'] = 100
if self.size_list['sw'] <= self.size_list['nw'] and self.size_list['sh'] <= self.size_list['nh']:
self.image_center()
self.image_set()
#=======================
#マウス移動
def mouse_move(self, event):
ssw = self.size_list['iw'] * (self.size_list['z'] / 100)
ssh = self.size_list['ih'] * (self.size_list['z'] / 100)
if self.size_list['fit'] == True or (ssw < self.size_list['nw'] and ssh < self.size_list['nh']):
return False
mw = self.size_list['nw'] / 2
mh = self.size_list['nh'] / 2
#x=================
if self.size_list['nw'] < ssw:
ex = event.x - mw
if ex == 0:
pos_x = self.size_list['nw'] / 2
else:
pos_x = mw - (ssw - self.size_list['nw']) * (ex / self.size_list['nw']) * 1.3
else:
pos_x = self.size_list['nw'] / 2
#y=================
if self.size_list['nh'] < ssh:
ey = event.y - mh
if ey == 0:
pos_y = self.size_list['nh'] / 2
else:
pos_y = mh - (ssh - self.size_list['nh']) * (ey / self.size_list['nh']) * 1.15
else:
pos_y = self.size_list['nh'] / 2
self.canvas.coords(self.canvas_image, int(pos_x), int(pos_y))
#=======================
#画像番号 変更
def file_index_change(self, mode):
if mode == 0:
self.file_index = self.file_index + 1
if self.file_len <= self.file_index:
self.file_index = 0
else:
self.file_index = self.file_index - 1
if self.file_index < 0:
self.file_index = self.file_len - 1
#=======================
#左下
def info_set(self):
self.info_label["text"] = "No.{}/{}「{}」「width:{} height:{} ({}%)」".format(
(self.file_index + 1),
self.file_len,
self.img_name,
self.size_list['iw'],
self.size_list['ih'],
self.size_list['z']
)
#=======================
#ウィンドウ サイズ変更
def on_resize(self, event):
if event.widget == self.ImageViewer:
if (
int(self.size_list['nw']) == int(event.width) and
int(self.size_list['nh']) == int(event.height - int(self.size_list['f'] * 2.5))
):
return False
self.size_list['fit_z'] = 0
self.size_list['nw'] = event.width
self.size_list['nh'] = event.height - int(self.size_list['f'] * 2.5)
self.canvas.config(width=self.size_list['nw'], height=self.size_list['nh'])
self.image_center()
self.image_set()
#=======================
#画像表示位置を中央へ
def image_center(self):
self.size_list['pw'] = int(self.size_list['nw'] / 2)
self.size_list['ph'] = int(self.size_list['nh'] / 2)
#=======================
#画像変更
def image_set(self,mode=0):
cha_img = self.file_list2[self.file_index]
if os.path.isfile(cha_img) == False:
return False
sep_dir = cha_img.split(os.sep)
self.img_name = sep_dir.pop()
try:
self.def_image = Image.open(cha_img)
if self.def_image.mode == 'CMYK':
base_dir = os.path.dirname(sys.executable)
icc_file = os.path.join(base_dir, 'AdobeRGB1998.icc')
if os.path.isfile(icc_file) == False:
icc_file = './AdobeRGB1998.icc'
icc = self.def_image.info.get("icc_profile")
if icc:
srgb = io.BytesIO(icc)
self.def_image = ImageCms.profileToProfile(
self.def_image,
srgb,
icc_file,
renderingIntent=0,
outputMode="RGB")
change_pos = self.image_size_check(self.def_image)
if self.size_list['fit'] == False:
zoom_num = self.size_list['z'] / 100
self.def_image = self.def_image.resize((int(self.size_list['iw'] * zoom_num),int(self.size_list['ih'] * zoom_num)))
else:
self.size_list['z'] = int(self.size_list['sw'] / self.size_list['iw'] * 100)
if change_pos['resize'] == True:
self.def_image = self.def_image.resize((change_pos['sw'],change_pos['sh']))
self.set_image = ImageTk.PhotoImage(self.def_image, master=self.ImageViewer)
if self.canvas_image is None:
self.canvas_image = self.canvas.create_image(change_pos['mw'], change_pos['mh'], image=self.set_image)
else:
self.canvas.itemconfig(self.canvas_image, image=self.set_image)
self.canvas.coords(self.canvas_image, self.size_list['pw'],self.size_list['ph'])
self.info_set()
except:
self.info_label["text"] ='「{}」この画像は表示できません'.format(self.img_name)
#=======================
#画像サイズチェック
def image_size_check(self, ch_image):
change_pos = {'mw':0,'mh':0,'sw':0,'sh':0,'flag':False,'resize':False}
change_pos['mw'] = int(self.size_list['nw'] / 2)
change_pos['mh'] = int(self.size_list['nh'] / 2)
self.size_list['iw'] = ch_image.width
self.size_list['ih'] = ch_image.height
if self.size_list['nw'] < self.size_list['iw'] and self.size_list['nh'] < self.size_list['ih']:
change_pos['resize'] = True
if self.size_list['iw'] < self.size_list['ih']:
change_pos['sh'] = self.size_list['nh']
change_pos['sw'] = int(self.size_list['iw'] * (change_pos['sh'] / self.size_list['ih']))
else:
change_pos['sw'] = self.size_list['nw']
change_pos['sh'] = int(self.size_list['ih'] * (change_pos['sw'] / self.size_list['iw']))
elif self.size_list['nw'] < self.size_list['iw']:
change_pos['resize'] = True
change_pos['sw'] = self.size_list['nw']
change_pos['sh'] = int(self.size_list['ih'] * (change_pos['sw'] / self.size_list['iw']))
elif self.size_list['nh'] < self.size_list['ih']:
change_pos['resize'] = True
change_pos['sh'] = self.size_list['nh']
change_pos['sw'] = int(self.size_list['iw'] * (change_pos['sh'] / self.size_list['ih']))
#========================
if self.size_list['nh'] < change_pos['sh']:
change_pos['sh'] = self.size_list['nh']
change_pos['sw'] = int(self.size_list['iw'] * (change_pos['sh'] / self.size_list['ih']))
elif self.size_list['nw'] < change_pos['sw']:
change_pos['sw'] = self.size_list['nw']
change_pos['sh'] = int(self.size_list['ih'] * (change_pos['sw'] / self.size_list['iw']))
if change_pos['sw'] == 0:
self.size_list['sw'] = ch_image.width
self.size_list['sh'] = ch_image.height
else:
self.size_list['sw'] = change_pos['sw']
self.size_list['sh'] = change_pos['sh']
if self.size_list['fit_z'] == 0:
self.size_list['fit_z'] = int(self.size_list['sw'] / self.size_list['iw'] * 100)
return change_pos
#=======================
try:
main_img = sys.argv[1]
except:
sys.exit()
pyImg = pyImageViewer()

動作画面
機能の解説
機能解説になりますが、正直なところフォトへのアンチテーゼです。フォトビューアーにあってフォトでなくなった機能を搭載しています。
まず個人的にフォトで使いにくくなった点として
・画像送りがループしなくなった
・画像送り後にサイズが調整される
・マウスの4,5ボタンに対応していない
・拡大縮小がアニメーションする
・不要なボタンがめちゃくちゃ多い
特に困ったのはループとマウスの4,5ボタン。
ループはどうしようもないとして、
マウスだけで画像送りをしたければ一応画面隅に移動ボタンがあります。
が、なぜフォトビューアーでできた機能を削ったのかと。
なおフォトで4,5ボタンを使うと概ね左クリックと同じ挙動をします。
あと画像送り後の調整は画像の位置が変だったり拡大されていたりで非常に見辛い。
機能一覧
対応させた機能は
・画像クリックで原寸拡大
・ドラッグではなくマウスの移動だけでで閲覧位置の調整
・キーボードの左右キーとマウスの4,5ボタンで画像送り
・画像送りはループする
・ホイールで拡大縮小(最小10% ~ 最大100%)
あくまで個人用に作ったのでGUI面は放棄しています。
フォトビューアーでも原寸拡大ボタンぐらいしか使わなかったので。
4,5ボタンはボタン番号でしか検出できなかったので「進む/戻る」に対応しているかは不明です。
exeファイル化
最終的にpyinstallerを使ってexeファイルにしております。自己署名証明書を作成して署名もしておいたのですが、
その辺りの設定はこちらのページで非常にわかりやすく書かれていたのでリンクを掲載しておきます。
【PowerShell】自作の.exeファイルを自己署名を入れる方法 / しぐにゃもブログ
欠点
・起動が多少遅い(pyinstallerの設定による)
・ウインドウサイズを記録しないので、開く度に調整が必要
・メモリ消費量問題(後述)
・画像サイズ変更が重い
・CMYKの色調が変わる
このビューアーは画像を表示する時にPillowが一度メモリへ展開します。
つまり画像が大きければ大きいほどメモリを消費するということです。
初期ウインドウサイズであればだいたい20~40MBほどですが
6000pxぐらいの画像を原寸表示すると400MBぐらいは消費し始めます。
photoImageのzoomメソッドを使えばどうかと思ったのですが
pillowのImageTkで作ったphotoImageにはzoomメソッドが入ってないらしい。
canvasの見た目だけ拡大できるような機能があればいいのですけどね。
またCMYK画像を表示するにはadobeが公開しているiccを用意する必要があります。
exeと同フォルダに置いておけば読み込まれます。
iccファイルがない状態だと「この画像は表示できません」エラーになります。
そして表示できても多少暗い画像になります。
教訓
GUIアプリは割と簡単に作れる。Copyright © スペース・アイ株式会社