pythonで作成したfigをpngでクリップボードへ移したい

# はじめに

例えばJupyter上でmatplotlibを使ってグラフを作成し、それを別の場所に貼り付けるのが面倒だと思ったことはないでしょうか？いちいち保存してコピーするのは無駄な気がしますし、右クリックでコピーしてもなんだか画質が合わず調整も難しい。 **作成したグラフをいい感じにクリップボードへ送ることができれば、このストレスを低減できるのではないか！** そんなプログラムを試作した結果を記します。

以下、簡単な実行環境の紹介です。（そんなに依存性はないはず。）
* Windows 11
* Python 3.7
* PowerShell 5.1

# 2024/12/22追記

コメントにてより簡単な方法を教えて頂きました。基本的にはこっちでいいと思いますので、サンプルを載せておきます。

```python
import matplotlib.pyplot as plt
import io
import win32clipboard

# 適当なfigを作成する
fig, ax = plt.subplots(1, 1, figsize=(6, 4))

# メモリストリームにPNG形式で保存してから読み出す
buffer = io.BytesIO()
fig.savefig(buffer, format='PNG', transparent=True)
data = buffer.getvalue()
buffer.close()
# クリップボードをクリアして、データをセットする
clipboard_format = win32clipboard.RegisterClipboardFormat('PNG')
win32clipboard.OpenClipboard()
win32clipboard.EmptyClipboard()
win32clipboard.SetClipboardData(clipboard_format, data)
win32clipboard.CloseClipboard()
```

以降は要らないものとなってしまいましたが、mmapの使い方を勉強した記録としてそのまま残しておきます。


# どうやるか

とはいえ、これぐらいのことであれば誰かやっている人がいるだろうと思い調べてみました。まず良さそうだと思ったのは[こちらの記事](https://qiita.com/flip-flop/items/89cd4c52309d1d0641d6 "スクリーンショットをクリップボードに保存")で紹介されている方法です。BMPでクリップボードに送る都合上、透過部分はうまくコピーできず、Jupyter側とdpiを調整しないと画質や大きさの変な画像がコピーされてしまいますが、それらが使用上問題なければこの方法が早いです。

透過の情報を保ったまま画像をコピーする方法はないかと調べていたところ、[こちらの記事](https://teratail.com/questions/r22ual4vgf98oo "PowerShellでアルファチャンネルを維持したまま 特定画像ファイルからクリップボードに画像を入れたい。")を見つけました。PowerShell経由でPNGの画像をクリップボードにコピーできれば、やりたいことができそうです。PowerShellならps1ファイルを置いておいてPythonのSubprocessから実行できるので、モジュール的な感じで処理もまとめられます。

あとはどうやってPythonからPowerShellのプログラムに画像を渡すかですが、一番簡単なのは一時ファイルとして一度保存したものを拾う方法だと思います。今回は[名前付き共有メモリでできそうな方法](https://qiita.com/Yusuke_Pipipi/items/585302b72363efb0f713 "Python C# データ連携")を見つけたので、こちらでやってみます。

というわけで長くなりましたが、作戦は以下です。最終的にはPowerShell部分もPythonから呼び出し、1～5をPythonの関数として実行できる形にします。

1. グラフをバイト列へ変換（Python）
2. バイト列を名前付き共有メモリへ書き込む（Python）
3. 名前付き共有メモリからバイト列を読み取る（PowerShell）
4. バイト列をPNGに変換（PowerShell）
5. PNGをクリップボードにコピー（PowerShell）

# プログラム

python側のプログラムです。適当なpathの通ったフォルダに置いておきます。引数はmatplotlibで作成したfig、共有メモリの名前tagname、後はfig.savefigに渡すkwardsです。PowerShellのプログラムを呼び出す部分ではデータ長と名前を渡しています。

```python:fig2clipboard.py
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の部分以外は[参考にしたサイト](https://qiita.com/Yusuke_Pipipi/items/585302b72363efb0f713 "Python C# データ連携")のままです。
```PowerShell:bmp2clipboard.ps1
$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がクリップボードに保存されます。
```python:test.py
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をクリップボードに送る簡単な方法が実は存在し、それを見落としていただけだったら悲しいですが、色々勉強になったので良しとします。もっとエレガントな方法を思い付いた方がいましたら（優しく）教えて頂けると嬉しいです。ではまた。