Pythonでクリップボードの画像をファイルに保存する
Windowsのクリップボード上の画像をファイルに保存するプログラムをPythonで書いてみた。Python 2.5.2とIronPython 1.1.1を使った。ちなみに私はPython初心者である。
やりたいこと
私はWindows上で、Altキー+Print Screenキーで、アクティブ・ウィンドウのイメージをクリップボードに保存し、それをファイルに保存するということをよくやる。Windowsに備わっているものだけでやるには、ペイントを起動して画像を貼り付け、それを保存する。ファイル形式は言うまでもなくPNGである。たまにしかやらないならペイントを使ってもいいが、やはり不便だ。なぜなら
- ペイントは前回保存したフォルダーを覚えてくれない。毎回マイ ピクチャへ保存しようとする。それ以外の場所に保存したいとなると、毎回フォルダーを指定しなければならない。
- 前回保存したファイル形式も覚えてくれない。PNGで保存するには、毎回ファイル形式を選択しなければならない。
そもそも、クリップボード上の画像をそのままファイルに保存することを最低限の手間で行いたい。ペイントを使った以下の手順には改善の余地がある。
- ペイントを起動
- クリップボードの画像を貼り付ける
- ファイル保存を指示
- ペイントを終了
そこで以下のような機能のプログラムを作ってずっと使っている。今回それをPythonで書いてみたのである。
- 起動するとクリップボードに画像が入っているかどうか調べ、入っていなかったら終了する。
- ファイル保存・ダイアログ・ボックスを表示してファイル名を指定させる。最初に表示されるフォルダーは前回保存したフォルダーとなる。
- 指定されたファイルに指定されたファイルの拡張子に応じた形式で画像を保存してプログラムを終了する。
Python 2.5でクリップボード上の画像にアクセス
まずは、どうやってクリップボード上に画像があるか、そして、そこから画像データを取り出すかを調べた。
import win32clipboard as cb cb.OpenClipboard() if cb.IsClipboardFormatAvailable(cb.CF_BITMAP): image = cb.GetClipboardData(cb.CF_BITMAP) print 'image read'
これをPython 2.5.2で実行すると、以下のようなエラーが起こる。
Traceback (most recent call last): File "clipboard-image-test1.py", line 4, in <module> image = cb.GetClipboardData(cb.CF_BITMAP) pywintypes.error: (6, 'GetClipboardData:GlobalLock', '\x83n\x83\x93\x83h\x83\x8b\x82\xaa\x96\xb3\x8c\xf8\x82\xc5\x82\xb7\x81B')
エラーメッセージにある文字列をprintすると、「ハンドルが無効です。」となる。IsClipboardFormatAvailable()はCF_BITMAPに対して真なのに、いざデータを取り出そうとするとエラーが起こる。これはwin32clipboardのバグとしか思えない。CF_BITMAPがだめならCF_DIBではどうかと思いやってみた。
import win32clipboard as cb cb.OpenClipboard() if cb.IsClipboardFormatAvailable(cb.CF_DIB): image = cb.GetClipboardData(cb.CF_DIB) print 'image read'
こんどはエラーが起こらない。
クリップボード上の画像を保存する
そこで、PIL (Python Image Library)をインストールした上で、以下を実行してみた。もちろん、実行前にAlt+Print Screenを押して。
import sys from StringIO import StringIO import win32clipboard as cb from BmpImagePlugin import DibImageFile def main(): cb.OpenClipboard() if not cb.IsClipboardFormatAvailable(cb.CF_DIB): print 'no image in clipboard' return imgData = cb.GetClipboardData(cb.CF_DIB) imgFile = StringIO(imgData) img = DibImageFile(imgFile) fp = open('image.bmp', 'w') img.save(fp) fp.close() main()
このプログラムは一応動作する。しかし、保存されたファイルは以下のように表示され、少しおかしい。GetClipboardData()が返す値がおかしいのだろう。(末尾の2008-08-08追記を参照されたし)
IronPythonでやってみる
この時点で、普通のPython(CPython)を使うのをあきらめて、IronPythonを使ってみることにした。IronPythonは.NETで書かれたPythonで、.NETのライブラリーがすべて使える。
import re import clr clr.AddReference('System.Windows.Forms') from System.Windows.Forms import Clipboard, SaveFileDialog, DialogResult, \ MessageBox clr.AddReference('System.Drawing') from System.Drawing import Image from System.Drawing.Imaging import ImageFormat extToFormat = { 'bmp': ImageFormat.Bmp, 'gif': ImageFormat.Gif, 'jpg': ImageFormat.Jpeg, 'jpeg': ImageFormat.Jpeg, 'png': ImageFormat.Png, 'tif': ImageFormat.Tiff, 'tiff': ImageFormat.Tiff, } defaultExt = 'png' def fileNameToFormat(fileName): m = re.compile(r'\.\w+$').search(fileName) if not m: return None ext = fileName[m.start()+1:m.end()].lower() if extToFormat.has_key(ext): return extToFormat[ext] else: return None def main(): if not Clipboard.ContainsImage(): MessageBox.Show('Clipboard does not have an image.') return image = Clipboard.GetImage() dialog = SaveFileDialog() dialog.Filter = 'PNG files (*.png)|*.png|All files(*.*)|*.*' dialog.FilterIndex = 1 dialog.RestoreDirectory = True if dialog.ShowDialog() == DialogResult.OK: fileName = dialog.FileName format = fileNameToFormat(fileName) if not format: fileName += "." + defaultExt format = extToFormat[defaultExt] image.Save(fileName, format) main()
少し長くなっているのはファイル名の指定の部分と、入力されたファイル名の拡張子からファイル形式を判断している部分があるからである。入力されたファイル名に拡張子がない場合は、ファイル名に「.png」を加えるようになっている。
実際に使うのに、私は以下の内容のショートカットを作って、それをクイックローンチに入れている。
C:\IronPython\ipy.exe 上記のプログラムへのパス
Perl版との比較
従来使っていたのはActivePerl+ImageMagickのプログラムで、それに比べると、IronPython版はややポータビリティーが高い。多くの場合.NETは既に入っているだろうから、新たにインストールするのはIronPythonだけで、これが実現できるからである。それに対してPerl版を動かすには、ActivePerlとImageMagickをインストールしなければならない。
このプログラムは常駐するのではなく、画像を保存するたびに起動して、保存したら終了するようになっている。IronPython版は.NETなので起動にやや時間がかかる。
2008-08-08追記
コメントで、C Python版で、open(..., 'w') ではなく、open(..., 'wb')とすればいいとのご指摘をいただいた。その通りだった。そして、main()の最後はcb.CloseClipboard()をするのが行儀がいい。