はじめに
例えばJupyter上でmatplotlibを使ってグラフを作成し、それを別の場所に貼り付けるのが面倒だと思ったことはないでしょうか?いちいち保存してコピーするのは無駄な気がしますし、右クリックでコピーしてもなんだか画質が合わず調整も難しい。 作成したグラフをいい感じにクリップボードへ送ることができれば、このストレスを低減できるのではないか! そんなプログラムを試作した結果を記します。
以下、簡単な実行環境の紹介です。(そんなに依存性はないはず。)
- Windows 11
- Python 3.7
- PowerShell 5.1
どうやるか
とはいえ、これぐらいのことであれば誰かやっている人がいるだろうと思い調べてみました。まず良さそうだと思ったのはこちらの記事で紹介されている方法です。BMPでクリップボードに送る都合上、透過部分はうまくコピーできず、Jupyter側とdpiを調整しないと画質や大きさの変な画像がコピーされてしまいますが、それらが使用上問題なければこの方法が早いです。
透過の情報を保ったまま画像をコピーする方法はないかと調べていたところ、こちらの記事を見つけました。PowerShell経由でPNGの画像をクリップボードにコピーできれば、やりたいことができそうです。PowerShellならps1ファイルを置いておいてPythonのSubprocessから実行できるので、モジュール的な感じで処理もまとめられます。
あとはどうやってPythonからPowerShellのプログラムに画像を渡すかですが、一番簡単なのは一時ファイルとして一度保存したものを拾う方法だと思います。今回は名前付き共有メモリでできそうな方法を見つけたので、こちらでやってみます。
というわけで長くなりましたが、作戦は以下です。最終的にはPowerShell部分もPythonから呼び出し、1~5をPythonの関数として実行できる形にします。
- グラフをバイト列へ変換(Python)
- バイト列を名前付き共有メモリへ書き込む(Python)
- 名前付き共有メモリからバイト列を読み取る(PowerShell)
- バイト列をPNGに変換(PowerShell)
- PNGをクリップボードにコピー(PowerShell)
プログラム
python側のプログラムです。適当なpathの通ったフォルダに置いておきます。引数はmatplotlibで作成したfig、共有メモリの名前tagname、後はfig.savefigに渡すkwardsです。PowerShellのプログラムを呼び出す部分ではデータ長と名前を渡しています。
import mmap
from io import BytesIO
import subprocess
import os
def fig2clipboard(fig, tagname='default', **kwards):
# figをバイト列へ
with BytesIO() as buf:
fig.savefig(buf, format='png', dpi=dpi, bbox_inches=bbox_inches, pad_inches=pad_inches, **kwards)
bi = buf.getvalue()
# バイト列を共有メモリへ
mm_size = len(bi)
mm = mmap.mmap(-1, mm_size, tagname=tagname)
mm[0:mm_size] = bi
# powershellでバイト列→bitmap→png→clipboard
cmd1 = f'cd {os.path.dirname(__file__)}'
cmd2 = r'powershell -NoProfile -ExecutionPolicy Unrestricted .\bmp2clipboard.ps1'
print(f'{cmd1} & {cmd2} {mm_size} {tagname}')
subprocess.run(f'{cmd1} & {cmd2} {mm_size} {tagname}', shell=True)
# 共有メモリを閉じる
mm.close()
こちらはPowerShell側のプログラムです。上のPythonのプログラムと同じフォルダに保存しておきます。(python側で__file__でフォルダ指定しているため。)あまり詳しくないので、共有メモリ→バイト列→PNGの部分以外は参考にしたサイトのままです。
$mmsize = $Args[0]
$tagname = $Args[1]
$source = @'
using System;
using System.IO;
using System.Windows;
using System.Windows.Media.Imaging;
using System.IO.MemoryMappedFiles;
namespace Test {
public class Program {
public static void SetClipboard(string mmsize, string tagname) {
// 共有メモリを開く
MemoryMappedFile share_mem = MemoryMappedFile.OpenExisting(tagname);
MemoryMappedViewAccessor accessor = share_mem.CreateViewAccessor();
// 共有メモリを読み取る
var size = int.Parse(mmsize);
byte[] imageBytes = new byte[size];
accessor.ReadArray<byte>(0, imageBytes, 0, imageBytes.Length);
// 解放
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
accessor.Dispose();
share_mem.Dispose();
// バイト列からBitmapImageを生成する
var stream = new MemoryStream(imageBytes);
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = stream;
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
// bitmap生成
var pngEnc = new PngBitmapEncoder();
pngEnc.Frames.Add(BitmapFrame.Create(bitmapImage));
using(var mem = new MemoryStream()){
pngEnc.Save(mem);
// コピー
Clipboard.SetData("PNG", mem);
}
}
}
}
'@
Add-Type -TypeDefinition $source -ErrorAction Stop -ReferencedAssemblies "System.Xaml", "PresentationCore", "WindowsBase"
[Test.Program]::SetClipboard($mmsize, $tagname)
使用例。適当なグラフを作成して、figをfig2clipboardに渡せばPNGがクリップボードに保存されます。
from fig2clipboard import fig2clipboard
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, 1, figsize=(3, 3), facecolor='none')
ax.plot([0, 1], [0, 1], lw=3)
fig2clipboard(fig)
おわりに
思ったより時間がかかってしまいました。pythonでpngをクリップボードに送る簡単な方法が実は存在し、それを見落としていただけだったら悲しいですが、色々勉強になったので良しとします。もっとエレガントな方法を思い付いた方がいましたら(優しく)教えて頂けると嬉しいです。ではまた。