0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Jupyterに画像を簡単に貼り付けるボタンを作ってみた

Posted at

はじめに

Jupyter Notebookはコードの他にMarkdown形式で説明文をセットにできることが特徴です。文章だけでなくExcelの資料でよく見かけるように画像もあった方がよりわかりやすいはずです。画像の添付方法は以下の3つが一般的ですが、それぞれデメリットがあります。

1. 画像のリンクをMarkdownセルに記入
→ 画像が外部リンクに依存するため、リンク切れのリスク

2. 画像の同一ディレクトリに設置しファイル名をMarkdownセルに記入 → ファイルが複数に分かれて他の人に共有するときに不便

3. コードセルに画像を表示する処理を記入(例: Imageモジュールなど) → 上と同様

4. 画像をBase64形式でエンコードしてMarkdownセルに記入 → ノートブックに埋め込まれるが、面倒

そこで、クリップボードにコピーした画像を簡単にMarkdownセルに貼り付けられるボタンを作成しました。以下にその実装と手順を紹介します。

社内向けにシンプルなJupyterを整備している関係でJupyter Notebook 6系向けに作成しており、Labベースの7は対応していません。

実行中の動画

ツールバーに「画像を挿入」ボタンが追加されます。このボタンを押すとクリップボードにコピーした画像が現在選択されているセルの下に挿入されます。

JupyterCopyPaste.gif

以下2点の作業例です

  • 画像を「いらすとや1」様からコピー&ペースト
  • スクリーンショットを撮影しそのまま貼り付け

ソースコードと設定方法

設置手順

以下のコードを~/.jupyter/custom/custom.jsに保存してください。Jupyter Notebookを再起動すると機能が有効になります。

保存先のディレクトリは、環境によって異なる場合がありますので注意してください。

custom.js
require(["base/js/namespace", "base/js/events"], function(Jupyter, events) {
    function addClipboardImageToMarkdownCell() {
        (async function() {
            try {
                const items = await navigator.clipboard.read();
                const imageItem = Array.from(items).find(item =>
                    Array.from(item.types).some(type => type.startsWith('image/'))
                );

                if (!imageItem) {
                    alert("画像をコピーしてから押してください");
                    return;
                }

                const mimeType = imageItem.types.find(type => type.startsWith('image/'));
                const blob = await imageItem.getType(mimeType);
                const reader = new FileReader();

                reader.onload = function(event) {
                    const img = new Image();
                    img.onload = function() {
                        const maxWidth = 800; // 最大横幅
                        const scale = Math.min(1, maxWidth / img.width); // 縮小スケールを計算
                        const canvas = document.createElement('canvas');
                        canvas.width = img.width * scale;
                        canvas.height = img.height * scale;

                        const ctx = canvas.getContext('2d');
                        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

                        let dataUrl = '';
                        let mimeTypeForMarkdown = '';
                        try {
                            dataUrl = canvas.toDataURL('image/webp', 0.8); // 圧縮品質を0.8に設定
                            mimeTypeForMarkdown = 'image/webp';
                        } catch (e) {
                            console.warn("WebP形式への変換に失敗しました。PNG形式にフォールバックします。", e);
                            dataUrl = canvas.toDataURL('image/png');
                            mimeTypeForMarkdown = 'image/png';
                        }

                        const base64Data = dataUrl.split(',')[1];
                        const buttonCellIndex = Jupyter.notebook.get_selected_index();
                        const newCell = Jupyter.notebook.insert_cell_at_index('markdown', buttonCellIndex + 1);
                        const imgMarkdown = `![クリップボード画像](data:${mimeTypeForMarkdown};base64,${base64Data})`;

                        newCell.set_text(imgMarkdown);
                        newCell.render();
                        Jupyter.notebook.select(buttonCellIndex + 1);
                        Jupyter.notebook.focus_cell();
                    };

                    img.onerror = function() {
                        alert("画像の読み込みに失敗しました。");
                    };

                    img.src = event.target.result;
                };

                reader.readAsDataURL(blob);
            } catch (err) {
                alert("エラー: クリップボードから画像を取得できません。");
                console.error(err);
            }
        })();
    }

    function addButton() {
        Jupyter.toolbar.add_buttons_group([
            {
                label: '画像を挿入',
                icon: 'fa-paste', // FontAwesomeアイコン
                callback: addClipboardImageToMarkdownCell
            }
        ]);
    }

    // Notebookが完全にロードされた後にボタンを追加
    if (Jupyter.notebook._fully_loaded) {
        // Notebookがすでにロード済みの場合
        addButton();
    } else {
        // Notebookのロード完了イベントを待つ
        events.on("notebook_loaded.Notebook", addButton);
    }
});

ポイント解説

1.クリップボードからの画像取得

ファイルダイアログから画像を選択するのではなく、コピペする方法にしました。ローカルに保存済みの画像以外でもスムーズに作業できるかと思います。

  • Webサイトの画像
  • Excel、PDF等ドキュメント内の画像
  • スクリーンショット

2.画像のリサイズと圧縮

BASE64の文字数には限度があるため、画像の最大容量を制限する仕組みが必要でした。そこで最大横幅を800pxにリサイズし、WebP形式で圧縮するようにしました。いろいろ試しましたがWebP形式がサイズが一番小さかったです。当初画像変換系のライブラリが必要かと思いましたがcanvasを使用することでJavaScriptだけで済みました。

  1. (参考)かわいいフリー素材集 いらすとや https://www.irasutoya.com/

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?