Image support for the async clipboard API
The Asynchronous Clipboard API now handles some images, in addition to text.
Chrome 66 added support for the text portion of the Asynchronous Clipboard API. Chrome 76 has added support for images, making it easier to programmatically copy and paste PNG images.
Caution: At the time of writing, only PNG files are supported. Support for other images and file formats will be added in the future.
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
Before describing image support, I want to review how the Asynchronous Clipboard API works. Feel free to skip ahead if you're already comfortable
Copy: writing text to the clipboard
To copy text to the clipboard, call navigator.clipboard.writeText()
.
Since this API is asynchronous, the writeText()
function returns a promise
that resolves or rejects depending on whether the passed text is
copied successfully. For example:
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 {
let 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.
Note that you need to call preventDefault()
in order to modify the
to-be-pasted data, like for example, convert it to uppercase before pasting.
It works nicely with the new asynchronous methods for reading clipboard text:
document.addEventListener('paste', async (e) => {
e.preventDefault();
try {
let text = await navigator.clipboard.readText();
text = text.toUpperCase();
console.log('Pasted UPPERCASE text: ', text);
} catch (err) {
console.error('Failed to read clipboard contents: ', err);
}
});
Security and permissions
The navigator.clipboard
property 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 in 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);
};
Images in 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.
To write an image to the clipboard, you need the image as a Blob
.
One way to do this is by requesting the image from an server by calling
fetch()
(or XMLHTTPRequest()
). The response
object returned by fetch()
has a blob()
method and XMLHTTPRequest()
let's you set
"blob"
as the responseType
.
Calling the server may not be desireable or possible for a variety of reasons.
Fortunately, you can also write the image to a canvas, then call
HTMLCanvasElement.toBlob()
.
Next, pass an array of ClipboardItem
objects 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 flexible way
to do this by using the new dynamic property keys syntax.
The MIME type used as the key is retrieved from blob.type
. This approach ensures
that your code will be ready for future image types as well as other MIME types
that may be supported in the future.
try {
const imgURL = '/images/generic/file.png';
const data = await fetch(imgURL);
const blob = await data.blob();
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob
})
]);
console.log('Image copied.');
} catch(e) {
console.error(e, e.message);
}
Paste: reading an image from the clipboard
The navigator.clipboard.read()
method, which reads data from the clipboard, is
also asynchronous, and Promise-based.
To read an image from the clipboard, obtain a list of ClipboardItem
objects,
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 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
To dynamically handle paste events, listen for the paste
event, call
preventDefault()
to prevent the default behavior
in favor of your own logic, 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. As before, don't forget to call preventDefault()
.
document.addEventListener('copy', async (e) => {
e.preventDefault();
try {
for (const item of e.clipboardData.items) {
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob
})
]);
}
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 Chrome 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
Chrome is actively working on expanding the Asynchronous Clipboard API to add support a larger number of data types. Due to the potential risks Chrome is treading carefully. To stay up to date on Chrome's progress, you can star the bug to be notified about changes.
For now, image support can be used in Chrome 76 or later.
Happy copying and pasting!
Related links
- Explainer
- Raw Clipboard Access Design Doc
- Chromium bug
- Chrome Platform Status entry
- Technical Architecture Group (TAG) Review
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.