Image Support for the Async Clipboard API

In Chrome 66, we shipped the text portion of the Asynchronous Clipboard API. Now in Chrome 76, adding support for images to the Asynchronous Clipboard API, making it easy to programmatically copy and paste image/png images.

Before we dive in, let’s take a brief look at how the Asynchronous Clipboard API works. If you remember the details, skip ahead to the image section.

Recap of the Asynchronous Clipboard API

Copy: Writing text to the clipboard

Text can be copied to the clipboard by calling navigator.clipboard.writeText(). Since this API is asynchronous, the writeText() function returns a Promise that will be resolved or rejected depending on whether the passed text is copied successfully:

async function copyPageURL() {
  try {
    await navigator.clipboard.writeText(location.href);
    console.log('Page URL copied to clipboard');
  } catch (err) {
    console.error('Failed to copy: ', err);
  }
}

Paste: Reading text from the clipboard

Much like copy, text can be read from the clipboard by calling navigator.clipboard.readText() and waiting for the returned Promise to resolve with the text:

async function getClipboardText() {
  try {
    const text = await navigator.clipboard.readText();
    console.log('Clipboard contents: ', text);
  } catch (err) {
    console.error('Failed to read clipboard contents: ', err);
  }
}

Handling paste events

Paste events can be handled by listening for the (surprise) paste event. It works nicely with the new asynchronous methods for reading clipboard text:

document.addEventListener('paste', async (e) => {
  e.preventDefault();
  try {
    const text = await navigator.clipboard.readText();
    console.log('Pasted text: ', text);
  } catch (err) {
    console.error('Failed to read clipboard contents: ', err);
  }
});

Security and permissions

The navigator.clipboard API is only supported for pages served over HTTPS, and to help prevent abuse, clipboard access is only allowed when a page is the active tab. Pages in active tabs can write to the clipboard without requesting permission, but reading from the clipboard always requires permission.

When the Asynchronous Clipboard API was introduced, two new permissions for copy and paste were added to the Permissions API:

  • The clipboard-write permission is granted automatically to pages when they are the active tab.
  • The clipboard-read permission must be requested, which you can do by trying to read data from the clipboard.
const queryOpts = { name: 'clipboard-read' };
const permissionStatus = await navigator.permissions.query(queryOpts);
// Will be 'granted', 'denied' or 'prompt':
console.log(permissionStatus.state);

// Listen for changes to the permission state
permissionStatus.onchange = () => {
  console.log(permissionStatus.state);
};

🆕 The new image-focused portion of the Asynchronous Clipboard API

Copy: Writing an image to the clipboard

The new navigator.clipboard.write() method can be used for copying images to the clipboard. Like writeText(), it is asynchronous, and Promise-based. Actually, writeText() is just a convenience method for the generic write() method.

In order to write an image to the clipboard, you need the image as a Blob. One way to achieve this is by fetching (or XMLHttpRequesting) the image from a server and getting the response body as a Blob (or for XHR, by setting the responseType to 'blob'). Another method to Blobify an image is to write the image to a canvas, then call the canvas’s toBlob() method.

Next, pass an array of ClipboardItems as a parameter to the write() method. Currently you can only pass one image at a time, but we plan to add support for multiple images in the future.

The ClipboardItem takes an object with the MIME type of the image as the key, and the actual blob as the value. The sample code below shows a future-proof way to do this by leveraging the Object.defineProperty() method. Using this approach will ensure your code will be ready for future image types as well as other MIME types that the Asynchronous Clipboard API may support.

try {
  const imgURL = 'https://developers.google.com/web/updates/images/generic/file.png';
  const data = await fetch(imgURL);
  const blob = await data.blob();
  await navigator.clipboard.write([
    new ClipboardItem(Object.defineProperty({}, blob.type, {
      value: blob,
      enumerable: true
    }))
  ]);
  console.log('Image copied.');
} catch(e) {
  console.error(e, e.message);
}

Paste: Reading an image from the clipboard

The navigator.clipboard.read() method reads data from the clipboard. It is also asynchronous, and Promise-based.

To read an image from the clipboard, we need to obtain a list of ClipboardItems, then iterate over them. Since everything is asynchronous, use the for ... of iterator, since it handles async/await code nicely.

Each ClipboardItem can hold its contents in different types, so you'll need to iterate over the list of types, again using a for ... of loop. For each type, call the getType() method with the current type as an argument to obtain the corresponding image Blob. As before, this code is is not tied to images, and will work with other future file types.

async function getClipboardContents() {
  try {
    const clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
      try {
        for (const type of clipboardItem.types) {
          const blob = await clipboardItem.getType(type);
          console.log(URL.createObjectURL(blob));
        }
      } catch (e) {
        console.error(e, e.message);
      }
    }
  } catch (e) {
    console.error(e, e.message);
  }
}

Custom paste handler

If you want to dynamically handle paste events, you can listen for the paste event, prevent the default behavior, then use the code above to read the contents from the clipboard, and handle it in whatever way your app needs.

document.addEventListener('paste', async (e) => {
  e.preventDefault();
  getClipboardContents();
});

Custom copy handler

The copy event includes a clipboardData property with the items already in the right format, eliminating the need to manually create a blob. Like before, don't forget to prevent the default behavior.

document.addEventListener('copy', async (e) => {
  e.preventDefault();
  try {
    for (const item of e.clipboardData.items) {
      await navigator.clipboard.write([
        new ClipboardItem(Object.defineProperty({}, item.type, {
          value: item,
          enumerable: true
        }))
      ]);
    }
    console.log('Image copied.');
  } catch(e) {
    console.error(e, e.message);
  }
});

Demo

Security

Opening up the Asynchronous Clipboard API for images comes with certain risks that need to be carefully evaluated. One new challenge are so-called image compression bombs, that is, image files that appear to be innocent, but—once decompressed—turn out to be huge. Even more serious than large images are specifically crafted malicious images that are designed to exploit known vulnerabilities in the native operating system. This is why we can’t just copy the image directly to the native clipboard, and why in Chrome we require that the image be transcoded.

The specification therefore also explicitly mentions transcoding as a mitigation method: “To prevent malicious image data from being placed on the clipboard, the image data may be transcoded to produce a safe version of the image.” There is ongoing discussion happening in the W3C Technical Architecture Group review on whether, and how the transcoding details should be specified.

Next Steps

We are actively working on expanding the Asynchronous Clipboard API to add support a larger number of data types. But, due to the potential risks, we will tread carefully. You can star the bug to be notified about changes.

For now, image support has landed and can be used as of Chrome 76.

Happy copying & pasting!

Acknowledgements

The Asynchronous Clipboard API was implemented by Darwin Huang and Gary Kačmarčík. Darwin also provided the demo. My introduction of this article is inspired by Jason Miller’s original text. Thanks to Kyarik and again Gary Kačmarčík for reviewing this article.

Feedback

Was this page helpful?
Yes
What was the best thing about this page?
It helped me complete my goal(s)
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It had the information I needed
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It had accurate information
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It was easy to read
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
Something else
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
No
What was the worst thing about this page?
It didn't help me complete my goal(s)
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It was missing information I needed
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It had inaccurate information
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It was hard to read
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
Something else
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.

rss_feed Subscribe to our RSS or Atom feed and get the latest updates in your favorite feed reader!