描いたグラフや編集中の画像を「画像としてコピー」できると、チャットや文書にそのまま貼ってもらえます。書き込みは Clipboard Manager プラグインの writeImage()(Rust は write_image())で行います。ファイルのパス、PNG のバイト列、RGBA の生データなどを渡せますが、渡し方によって Cargo の image-png 機能の要否が変わり、JPEG はそのままでは扱えません。この違いと、どんな画像でもコピーできる canvas 経由の方法を説明します。テキストは クリップボードのテキストを読み書きする で扱います。
前提条件
npm run tauri add clipboard-manager
clipboard-manager:default は何も許可しないので、書き込みの clipboard-manager:allow-write-image を追加します。後述の読み取り(貼り付け)も使うなら clipboard-manager:allow-read-image も足します。Image.new() など画像オブジェクトを扱う関数の権限は core:image:default に含まれるので、core:default があれば追加は不要です。
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"clipboard-manager:allow-write-image",
"clipboard-manager:allow-read-image"
]
}
PNG のファイルやバイト列から画像を作るには、src-tauri/Cargo.toml の tauri に image-png 機能を付けます(ICO なら image-ico)。画像の読み込みは Rust 側で行われるため、JS から渡す場合でもこの設定が要ります。
[dependencies]
tauri = { version = "2", features = ["image-png"] }
writeImage() に渡せるものは次のとおりです。
| 渡すもの | 例 | image-png |
|---|---|---|
| ファイルのパス(文字列) | writeImage('C:/img/a.png') | 必要 |
| PNG / ICO のバイト列 | Uint8Array、ArrayBuffer | 必要 |
Image オブジェクト | Image.new()、readImage() の戻り値 | 作り方による |
Uint8Array や数値の配列は、常に「PNG などのファイルの中身」として扱われます。RGBA の生データを渡したいときは、幅と高さを添えて Image.new(rgba, width, height) で包みます。
1. フロントエンドから実装する (TypeScript)
canvas や表示中の画像をコピーする
JPEG や WebP、SVG は Rust 側では読み込めませんが、WebView なら表示できます。そこで一度 canvas に描き、PNG に変換してから渡せば、画面に出せる画像ならどれでもコピーできます。
import { writeImage } from '@tauri-apps/plugin-clipboard-manager';
// canvas の内容を PNG のバイト列にする
async function toPngBytes(canvas: HTMLCanvasElement): Promise<Uint8Array> {
const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, 'image/png'));
if (!blob) throw new Error('PNG に変換できませんでした');
return new Uint8Array(await blob.arrayBuffer());
}
export async function copyCanvas(canvas: HTMLCanvasElement) {
await writeImage(await toPngBytes(canvas)); // allow-write-image と image-png 機能が必要
}
// <img> に表示している画像(JPEG / WebP / SVG も可)をコピーする
export async function copyImageElement(img: HTMLImageElement) {
await img.decode(); // 読み込みの完了を待つ
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
canvas.getContext('2d')?.drawImage(img, 0, 0);
await copyCanvas(canvas);
}
Uint8Array は IPC で数値の配列に変換されて送られるので、大きな画像ほど時間がかかります。RGBA の生データ(幅 × 高さ × 4 バイト)で渡すより、PNG に圧縮した方が送る量はずっと小さくなります。
画像ファイルをコピーする
PNG ファイルならパスの文字列を渡すだけです。ファイルは Rust 側で読まれるので、fs プラグインは要りません。パスは ファイルを開くダイアログ で選ばせるか、同梱した画像 なら resolveResource() で絶対パスにします。相対パスはアプリの作業ディレクトリが基準になり、起動のしかたで変わるので避けます。
import { writeImage } from '@tauri-apps/plugin-clipboard-manager';
import { resolveResource } from '@tauri-apps/api/path';
export async function copyBundledImage() {
// bundle.resources に入れた images/logo.png の絶対パス
const path = await resolveResource('images/logo.png');
await writeImage(path); // PNG / ICO のみ。JPEG は読み込みに失敗する
}
クリップボードの画像を読み取る
readImage() は RGBA の生データを持つ Image を返します。PNG ではないので、new Blob([rgba]) のように画像ファイルとしては扱えません。ImageData にして canvas に描きます。Image の実体は Rust 側に残るので、使い終わったら close() で解放します。
import { readImage } from '@tauri-apps/plugin-clipboard-manager';
export async function pasteImageTo(canvas: HTMLCanvasElement) {
const image = await readImage(); // allow-read-image。画像が無ければ例外
try {
const { width, height } = await image.size();
const rgba = await image.rgba();
canvas.width = width;
canvas.height = height;
const data = new ImageData(new Uint8ClampedArray(rgba), width, height);
canvas.getContext('2d')?.putImageData(data, 0, 0);
} finally {
await image.close(); // Rust 側に残っている画像を解放する
}
}
2. バックエンドから実装する (Rust)
Rust では tauri::image::Image を作って app.clipboard().write_image(&image) に渡します。ファイルから作る Image::from_path() / Image::from_bytes() には image-png 機能が要ります。アプリに埋め込む小さな画像なら、tauri::include_image! でビルド時に RGBA へ変換しておけば実行時の読み込みは不要です(パスは Cargo.toml のあるフォルダーが基準)。RGBA のまま埋め込まれるので、大きな画像では実行ファイルが膨らみます。画素を自分で作るときは、左上から 1 行ずつ R・G・B・A の順に並べて Image::new_owned() に渡します。
use tauri::image::Image;
use tauri_plugin_clipboard_manager::ClipboardExt;
// ビルド時に RGBA へ変換して埋め込む
const LOGO: Image<'static> = tauri::include_image!("./icons/32x32.png");
#[tauri::command]
fn copy_logo(app: tauri::AppHandle) -> Result<(), String> {
app.clipboard().write_image(&LOGO).map_err(|e| e.to_string())
}
// PNG ファイルをコピーする(image-png 機能が必要)
#[tauri::command]
async fn copy_png_file(app: tauri::AppHandle, path: String) -> Result<(), String> {
let image = Image::from_path(&path).map_err(|e| format!("{path}: {e}"))?;
app.clipboard().write_image(&image).map_err(|e| e.to_string())
}
// 画素を自分で作る(8 ピクセルごとに赤と青が交互の 64 x 64)
#[tauri::command]
fn copy_stripes(app: tauri::AppHandle) -> Result<(), String> {
let (w, h) = (64u32, 64u32);
let mut rgba = Vec::with_capacity((w * h * 4) as usize);
for y in 0..h {
let px: [u8; 4] = if (y / 8) % 2 == 0 { [255, 0, 0, 255] } else { [0, 0, 255, 255] };
for _ in 0..w {
rgba.extend_from_slice(&px);
}
}
app.clipboard()
.write_image(&Image::new_owned(rgba, w, h))
.map_err(|e| e.to_string())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_clipboard_manager::init())
.invoke_handler(tauri::generate_handler![copy_logo, copy_png_file, copy_stripes])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';
await invoke('copy_png_file', { path: 'C:/Users/me/Pictures/chart.png' });
await invoke('copy_logo'); // Rust から書き込むので JS 側に allow-write-image は不要
Rust で read_image() を使う場合は、テキストの読み取りと同じくメインスレッドを避けて async fn のコマンドにします。Linux ではメインスレッドで読むとアプリが固まることがあります。
動作確認
npm run tauri dev で起動し、canvas に何か描いてから copyCanvas() を呼び、ペイントやチャットアプリに貼り付けると同じ画像が出ます。copy_stripes の後に貼ると赤と青の横縞になり、RGBA の並び順を確かめられます。読み取りは、ブラウザーなどで画像をコピーしてから次を実行します。
import { readImage } from '@tauri-apps/plugin-clipboard-manager';
const image = await readImage();
console.log(await image.size()); // 例: { width: 64, height: 64 }
await image.close();
よくあるエラーと対処法
- 「clipboard-manager.write_image not allowed. Permissions associated with this command: clipboard-manager:allow-write-image」: 権限の追加漏れです。リリースビルドでは「Command plugin:clipboard-manager|write_image not allowed by ACL」になります。
- 「expected RGBA image data, found raw bytes」(パスなら「found a file path」):
image-png機能が付いていません。Cargo.tomlに追加してビルドし直します。Image.fromBytes()では「from_bytes is only supported if theimage-icoorimage-pngCargo features are enabled」になります。 - 「failed to process image:」で始まるエラー: JPEG など PNG / ICO 以外を渡したか、RGBA の生データを
Image.new()で包まずに渡しています。canvas 経由で PNG にするか、Image.new(rgba, width, height)を使います。 - canvas の書き出しで「汚染された(tainted)canvas は書き出せない」という趣旨の SecurityError: 別オリジンの画像を描いた canvas は PNG にできません。
convertFileSrc()で表示した画像も別オリジン扱いなので、srcを設定する前にimg.crossOrigin = 'anonymous'を付けます。 readImage()で「The clipboard contents were not available in the requested format or the clipboard is empty.」: 画像が入っていません。テキストしか無い場合も同じです。
OS ごとの違いと注意点
- Android / iOS:
writeImage()とreadImage()は非対応で、「Unsupported on this platform」で失敗します。モバイルで扱えるのはテキストだけです。 - Linux: Rust から
read_image()を呼ぶときは、前述のとおりメインスレッドを避けます。 - 権限の範囲: パスを渡した
writeImage()は、fs プラグインのスコープと関係なくファイルを読みます。外部のページを表示するウィンドウにはallow-write-imageを付けません。 - サイズ: 画像は RGBA に展開されて渡るので、4K の画面全体なら約 33 MB になります。
