Webアプリでクリップボードから画像を貼り付ける

やりたかったこと

前回も書いた通り、ブログを作りました。

このブログには画像が貼れるのですが、今まではファイルをアップロードする形でしか画像を貼ることができませんでした。

どこかでスマホで撮ってきた写真を貼る、という使い方ならそれでよいですが、ちょっとしたスクショとか、amazonの書影とかを貼るときにいちいちファイルを作るのも面倒なので、クリップボードから貼り付けられるようにしました。画面としては、以下のように、「クリップボードから貼り付け」ボタンを追加しています。

このボタンは、普通に<button>タグで作ったHTML上のボタンで、これを押すとJavaScriptの関数が普通に呼び出されます。JavaScriptの関数が普通に実行されただけでクリップボードの中身がJavaScriptに抜かれてしまうのではセキュリティ的に大問題ですので、私が使っているブラウザであるFirefoxでは、「クリップボードから貼り付け」ボタンを押すとこんなふうに「貼り付け」ボタンが出て、ユーザが明示的にこれを押さないとクリップボードの内容はコピーされません。

Edgeの場合は、こんなポップアップが出ます。「許可」を押せばペーストできます。

クリップボードから画像を貼りたければ、現状、「contenteditable属性がtrueの要素に貼り付けさせる」という方法を取ることが多いようです。たとえばこの下にはcontenteditable属性がtrueのp要素が貼ってありますが、適当な画像をコピーして、ここをクリックしてフォーカスを当ててからCtrl-Vとかでペーストすると、画像が貼れます。

ここには画像が貼れる

こうしたうえで、この要素からimg要素を取り出すことで、画像が取り出せるそうです。私は試してませんが、検索するとQiitaで以下のような記事が見つかります。

クリップボードに貼り付けた画像を取得する javascript

JavaScriptでクリップボードの画像を取得する

この方法なら、ユーザが明示的にCtrl-Vを押すわけなので、前述のような警告は出ません。contenteditableがtrueの要素にフォーカスが当たっている状態なら使い勝手もよさそうですが、うちのブログのテキスト入力エリアは前回も書いたとおりただのtextareaなので、contenteditable属性がtrueの要素は画面上にありません。それを追加して、「ここにフォーカスを当ててからペーストしてください」とするのはいまいち迂遠なので1、今回はクリップボードAPIを使って直接クリップボードから画像を取り出すようにしました。

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

クリップボードから画像(バイナリデータ)を取り出すには、Clipboard APIのclipboard.read()メソッドを使います。使い方はMDNの以下のページに載っています。

Clipboard.read() - Web API | MDN

これを元にして作ったサンプルが以下になります。

まずはコピー元画像※2。別にこの画像でなくてもよいですが、右クリックしてコピーしてください。

ramen

次にこの下の「貼り付ける」ボタンを押すと、下のimg要素に画像が貼りつけられます。

これを行っているJavaScriptは以下。実物は、このページのHTMLの一番下にあります。

const destinationImage = document.querySelector("#destination");

async function pasteImage() {
  /*
  const permission = await navigator.permissions.query({
    name: "clipboard-read",
  });
  console.log("permission.state.." + permission.state);
  */

  const clipboardContents = await navigator.clipboard.read();
  for (const item of clipboardContents) {
    if (!item.types.includes("image/png")) {
      throw new Error("Clipboard contains non-image data.");
    }
    const blob = await item.getType("image/png");
    destinationImage.src = URL.createObjectURL(blob);
  }
}

4~9行目のコメントアウト部分はちょっと置いておいて、11行目でクリップボードの内容を取得しています。clipboard.read()の戻り値は(プロミス解決後は)ClipboardItemの配列とのことなので、そこからMIMEタイプがimage/pngの要素を取り出しています(16行目)。その後17行目でURL.createObjectURL()でオブジェクト化してURLを取得していますが、ここはこのページのデモ向けで、ブログでの画像貼り付けではここからファイルをアップロードしてしまうので使っていません。

コピー元の画像として上のラーメン画像を使った人は、「貼り付ける」ボタンを押したとき、Firefoxでは「貼り付け」のボタンが出ないことに気が付いたかもしれません。ブラウザ内で同一ページからコピーした画像では、このボタンは出ないようです。クリップボードにあるものがJavaScriptに勝手に取得されるのは危険ですが、同一ページのものなら問題ない、ということなのでしょう。なお、Edgeでは、同一ページのラーメン画像でもポップアップが出ました。

4~9行目部分はコメントアウトしていますが、このコメントアウトを外すと、Firefoxでは以下のエラーが出ます。

Uncaught (in promise) TypeError: 'clipboard-read' (value of 'name' member of PermissionDescriptor) is not a valid value for enumeration PermissionName.

これは、上記MDNのページに、以下のように書いてあるとおりの挙動ですね。

メモ: 現時点では、Firefoxは read() を実装していますが、 "clipboard-read" 権限を認識しないため、権限 API を使ってアクセス管理をしようとしてもうまくいきません。

Edgeだとエラーにならずにちゃんと動いて、8行目のconsole.log()が以下を出力します。

permission.state..prompt

これは、5行目のpermissions.query()で取得したPermissionStatusのstateで、 'granted'(許可)、'denied'(拒否)、'prompt'(プロンプト)のいずれかとのことです。これがpromptだから確認のポップアップが出るのだと思います。

取得した画像をサーバにアップロード

今回の目的は、ブログに画像を貼ることです。なので、画像を取得したら、それをサーバにアップロードしなければいけません。

このブログでは、もともとファイルを複数選んで画像をアップロードできます。クリップボードからコピペする場合、画像は1枚ですが、サーバ側の同じインタフェースをつかってアップロードしたいところです。

現在、画像アップロード時のfetch()には以下のRequestオブジェクトを渡していて、

  let req = new Request(url, {
      body: formData,
      method: "POST",
      mode: "no-cors"
    });

ここでbodyに設定しているformDataは、以下のように作っています。

// <input type="file"~が変更されたときに呼ばれるコールバック
function imageFileInputOnChange(event) {
  const files = event.target.files;

  const formData = new FormData();
  for (let i = 0; i < files.length; i++) {
    formData.append("file" + i, files[i]);
  }
  …

multipart/form-dataのFormDataに、file+{連番}というnameで※3、event.target.filesで取得したファイルを順にくっつけています。

これと同等のFormDataを作ったら、こうなりました。

  const file = new File([blob], "dummy.png");
  formData.append("file0", file);

Fileのコンストラクタの第1引数に渡している「[blob]」ですが、これを囲む配列の[]がないと以下のエラーが出ます。

Uncaught (in promise) TypeError: File constructor: Argument 1 can't be converted to a sequence.

仕様を見るとコンストラクタの第1引数は「反復可能オブジェクト」とのことなので配列にしなければいけないのかもしれませんが、なぜこうしなければいけないのか、私はいまいちわかっていません。Stackoverflowのこのあたりの質問と回答を見て、こうすれば動くのか、と思った次第です――だから型のはっきりしない言語って嫌いなんだ。

公開日: 2024/11/03



不具合等ありましたら、掲示板にご連絡願います。

ひとつ前 | ひとつ上のページへ戻る | トップページへ戻る