"""
クリップボードの内容(テキストまたは画像)を取得し、ファイルに保存するスクリプト。
このスクリプトはWindows API (User32, Ole32, Kernel32) を使用してクリップボードデータを処理し、
PIL (Pillow) ライブラリを使って画像を保存します。
テキストデータはUTF-8エンコーディングで保存され、DIB (Device Independent Bitmap) 形式の画像はPNG形式で保存されます。
ファイル保存後、reStructuredText (rst) 形式でそのファイルを参照するためのスニペットを標準出力に表示します。
Usage:
python save_clipboard.py [出力ファイル名のベース (拡張子なし)]
例:
# クリップボードの内容を 'my_output.txt' または 'my_output.png' として保存
python save_clipboard.py my_output
# 引数なしの場合、'clipboard_output.txt' または 'clipboard_output.png' として保存
python save_clipboard.py
:doc:`save_clipboard_usage`
"""
import ctypes
import ctypes.wintypes as wintypes
from ctypes import POINTER, byref, windll, c_void_p
from PIL import Image
import io
import struct
import sys
import os
# Windows APIの準備
ole32 = windll.ole32
kernel32 = windll.kernel32
user32 = windll.user32 # テキスト取得用に追加
# --- 64bitアドレスを正しく扱うための型定義 ---
kernel32.GlobalLock.argtypes = [c_void_p]
kernel32.GlobalLock.restype = c_void_p
kernel32.GlobalUnlock.argtypes = [c_void_p]
kernel32.GlobalSize.argtypes = [c_void_p]
kernel32.GlobalSize.restype = ctypes.c_size_t
# テキスト取得用API定義
user32.OpenClipboard.argtypes = [wintypes.HWND]
user32.OpenClipboard.restype = wintypes.BOOL
user32.CloseClipboard.restype = wintypes.BOOL
user32.GetClipboardData.argtypes = [wintypes.UINT]
user32.GetClipboardData.restype = wintypes.HANDLE
user32.IsClipboardFormatAvailable.argtypes = [wintypes.UINT]
user32.IsClipboardFormatAvailable.restype = wintypes.BOOL
# 定数定義
TYMED_HGLOBAL = 1
DVASPECT_CONTENT = 1
CF_DIB = 8
CF_UNICODETEXT = 13 # WindowsのUnicodeテキスト形式
GMEM_MOVEABLE = 0x0002
[ドキュメント]
class STGMEDIUM(ctypes.Structure):
"""
OLEデータ転送で使用されるストレージメディア構造体。
クリップボードやOLEオブジェクトからのデータ転送時に、実際のデータ
(ハンドル、ポインタ、ストリームなど)とそのタイプを保持します。
`u` メンバは、`tymed` の値に応じてHGLOBALハンドルなどを格納するために `c_void_p` として定義されています。
"""
_fields_ = [
("tymed", wintypes.DWORD),
("u", c_void_p), # data handle (HGLOBAL)
("pUnkForRelease", c_void_p),
]
[ドキュメント]
class IDataObject(ctypes.Structure):
"""
OLEデータ転送インターフェースの抽象基底クラス。
クリップボードやドラッグ&ドロップ操作など、データ転送オブジェクトが提供するデータを
管理するためのCOMインターフェースです。主に `GetData` メソッドを通じてデータを取得します。
"""
pass
IDataObject._fields_ = [("lpVtbl", POINTER(c_void_p))]
[ドキュメント]
def get_clipboard_text():
"""
クリップボードからUnicodeテキストを取得します。
User32.dllのOpenClipboard, GetClipboardData, GlobalLock, GlobalUnlock, CloseClipboard関数を使用します。
CF_UNICODETEXT形式のデータを取得し、Pythonの文字列として返します。
クリップボードがロックされている場合や、テキストデータが見つからない場合はNoneを返します。
:returns: クリップボード上のテキストデータ (`str`)、またはNone。
:rtype: str | None
"""
if not user32.OpenClipboard(None):
return None
try:
if not user32.IsClipboardFormatAvailable(CF_UNICODETEXT):
return None
h_global = user32.GetClipboardData(CF_UNICODETEXT)
if not h_global:
return None
ptr = kernel32.GlobalLock(c_void_p(h_global))
if not ptr:
return None
try:
# Unicode (UTF-16) としてデータを読み込む
text = ctypes.c_wchar_p(ptr).value
return text
finally:
kernel32.GlobalUnlock(c_void_p(h_global))
finally:
user32.CloseClipboard()
[ドキュメント]
def get_clipboard_image_fixed():
"""
クリップボードからDIB (Device Independent Bitmap) 形式の画像を取得し、PIL Imageオブジェクトとして返します。
Ole32.dllのOleGetClipboardとReleaseStgMedium関数、およびIDataObjectインターフェースのGetDataメソッドを使用します。
取得したDIBデータは、BMPファイルヘッダを追加してPIL (Pillow) で読み込み可能な形式に変換されます。
この関数を呼び出す前に `ole32.OleInitialize(None)` が呼び出されている必要があります。
:returns: クリップボード上の画像データ (`PIL.Image.Image`)、またはNone。
:rtype: PIL.Image.Image | None
"""
# 既にOleInitializeされている前提、または明示的に呼ぶ
# ole32.OleInitialize(None) # メイン側で制御
data_obj_ptr = POINTER(IDataObject)()
hr = ole32.OleGetClipboard(byref(data_obj_ptr))
if hr != 0:
return None
try:
# IDataObject::GetData のVtableインデックスは通常3
get_data_addr = data_obj_ptr.contents.lpVtbl[3]
GetDataProto = ctypes.WINFUNCTYPE(
ctypes.HRESULT,
POINTER(IDataObject),
POINTER(FORMATETC),
POINTER(STGMEDIUM)
)
get_data_func = GetDataProto(get_data_addr)
except (IndexError, AttributeError):
return None
fmt = FORMATETC()
fmt.cfFormat = CF_DIB
fmt.ptd = None
fmt.dwAspect = DVASPECT_CONTENT
fmt.lindex = -1
fmt.tymed = TYMED_HGLOBAL
stg = STGMEDIUM()
hr = get_data_func(data_obj_ptr, byref(fmt), byref(stg))
img = None
if hr == 0 and stg.tymed == TYMED_HGLOBAL:
h_global = stg.u
ptr = kernel32.GlobalLock(c_void_p(h_global))
size = kernel32.GlobalSize(c_void_p(h_global))
if ptr:
try:
dib_data = ctypes.string_at(ptr, size)
if len(dib_data) >= 40:
header_size = struct.unpack("<I", dib_data[0:4])[0]
offset_to_pixels = 14 + header_size
file_size = 14 + len(dib_data)
bmp_header = struct.pack("<2sIHHI", b"BM", file_size, 0, 0, offset_to_pixels)
img = Image.open(io.BytesIO(bmp_header + dib_data))
img.load() # メモリ上にデータをロード
finally:
kernel32.GlobalUnlock(c_void_p(h_global))
ole32.ReleaseStgMedium(byref(stg))
return img
[ドキュメント]
def print_rst_snippets_for_image(filepath):
"""
指定された画像ファイル用のreStructuredText (rst) スニペットを表示します。
ファイルリンク、`image` ディレクティブ、`figure` ディレクティブの例を生成し、
標準出力に出力します。これはSphinxなどでドキュメントを作成する際に役立ちます。
:param filepath: スニペットを生成する画像ファイルのパス。
:type filepath: str
:returns: なし
:rtype: None
"""
filename = os.path.basename(filepath)
rel_path = "./" + filename # 相対パスと仮定
print("\n" + "="*60)
print(f"--- reStructuredText (rst) 用表示コード ({filename}) ---")
print("="*60)
print("\n1. シンプルなファイルリンク:")
print(f"`{filename} <{rel_path}>`_")
print("\n2. 画像表示 (imageディレクティブ):")
print(f".. image:: {rel_path}\n :alt: {filename}")
print("\n3. 図表表示 (figureディレクティブ - キャプション付き):")
print(f".. figure:: {rel_path}\n :alt: {filename}\n :align: center\n\n {filename} の図面")
print("="*60 + "\n")
[ドキュメント]
def print_rst_snippets_for_text(filepath):
"""
指定されたテキストファイル用のreStructuredText (rst) スニペットを表示します。
`dropdown` (sphinx-design拡張機能が必要)、`parsed-literal`、`literalinclude`
ディレクティブの例を生成し、標準出力に出力します。
言語ヒントはファイル拡張子から推測されます。
:param filepath: スニペットを生成するテキストファイルのパス。
:type filepath: str
:returns: なし
:rtype: None
"""
filename = os.path.basename(filepath)
rel_path = "./" + filename # 相対パスと仮定
# ファイル拡張子から言語を推測(簡易版)
_, ext = os.path.splitext(filename)
lang_map = {'.py': 'python', '.js': 'javascript', '.bat': 'bat', '.sh': 'bash', '.cpp': 'cpp', '.h': 'cpp', '.txt': 'text'}
language = lang_map.get(ext.lower(), 'text')
print("\n" + "="*60)
print(f"--- reStructuredText (rst) 用表示コード ({filename}) ---")
print("="*60)
print("\n1. 折り畳み表示 (sphinx-design dropdown) ※要拡張機能:")
print(f".. dropdown:: {filename} の内容を表示(クリックで展開)\n :color: info\n :icon: file-code\n")
print(f" .. literalinclude:: {rel_path}\n :language: {language}\n :linenos:")
print("\n2. <pre>タグ相当での埋め込み (parsed-literal):")
print(".. parsed-literal::\n")
try:
with open(filepath, "r", encoding="utf-8") as f:
# 最初の数行だけサンプルとして表示
lines = f.readlines()
for line in lines[:5]:
print(f" {line.rstrip()}")
if len(lines) > 5:
print(" ...")
except Exception:
print(" (ファイル内容の読み込みに失敗しました)")
print("\n3. 通常の外部ファイル取り込み (literalinclude):")
print(f".. literalinclude:: {rel_path}\n :language: {language}")
print("="*60 + "\n")
if __name__ == "__main__":
# OleInitializeはプロセス内で1回呼ぶ必要がある(OLE API使用のため)
ole32.OleInitialize(None)
# デフォルトのファイル名ベース
base_filename = "clipboard_output"
# 引数がある場合はそれをファイル名にする
if len(sys.argv) > 1:
# 安全なファイル名にするための簡易処理(パス区切り文字などを除去)
requested_name = sys.argv[1]
base_filename = os.path.splitext(os.path.basename(requested_name))[0]
try:
print(f"--- クリップボードの内容を確認中... ---")
# 1. まずテキストがあるか確認(優先度が高い場合が多い)
text_content = get_clipboard_text()
if text_content is not None:
print("[情報] クリップボード内にテキストが見つかりました。")
# テキストファイルとして保存
out_name = f"{base_filename}.txt"
# Windowsの改行コード(CRLF)で保存
with open(out_name, "w", encoding="utf-8", newline='\r\n') as f:
f.write(text_content)
print(f"成功: テキストを '{out_name}' として保存しました (UTF-8)。")
print(f"文字数: {len(text_content)}")
# rstスニペット表示
print_rst_snippets_for_text(out_name)
sys.exit(0)
# 2. 次に画像があるか確認
img = get_clipboard_image_fixed()
if img:
print("[情報] クリップボード内に画像(DIB)が見つかりました。")
# 画像ファイルとして保存 (PNG推奨)
out_name = f"{base_filename}.png"
img.save(out_name, "PNG")
print(f"成功: 画像を '{out_name}' として保存しました。")
print(f"画像サイズ: {img.size[0]}x{img.size[1]}, モード: {img.mode}")
# rstスニペット表示
print_rst_snippets_for_image(out_name)
sys.exit(0)
# 3. どちらもない場合
print("[情報] クリップボードにテキストまたは画像(CF_DIB)は見つかりませんでした。")
# 形式リストを取得して表示(デバッグ用)
if user32.OpenClipboard(None):
try:
formats = []
fnum = user32.EnumClipboardFormats(0)
while fnum:
formats.append(fnum)
fnum = user32.EnumClipboardFormats(fnum)
if formats:
print(f"利用可能な形式ID: {formats}")
finally:
user32.CloseClipboard()
except Exception as e:
import traceback
print(f"\n[エラー] 予期せぬエラーが発生しました。")
traceback.print_exc()
finally:
# OleUninitializeはプロセス終了時に呼ぶ
ole32.OleUninitialize()