Copy & Paste Images – Real-World PWA: The Making Of Paint.Js.Org – Part 3

In part three of the series about the making of the web-based Microsoft Paint clone, I want to show how you can copy drawings from the Paint clone to other applications and paste them back.

In this article:

Christian Liebel is consultant at Thinktecture, focuses on web standards and Progressive Web Applications, and is Thinktecture's representative at the W3C.

In the previous part of this series, I showed how you can use the HTML canvas element to implement the drawing area. In order to copy and paste images, the pixel-based image data needs to be converted to and from an exchange format such as PNG. For the copy and paste part, I will demonstrate the use of the Async Clipboard API. This API is supported on Google Chrome, other Chromium-based browsers such as Opera or Edge since version 62, and on Apple Safari starting from version 13.1 (support table on At the time of this writing, Mozilla Firefox only allows writing plain text to the clipboard.

Image to Blob

Before we can copy to another application, it needs to be converted to an exchange format. The most compatible format that can be used in combination with the Async Clipboard API across all platforms is the PNG format (Portable Network Graphics). The canvas API offers a toBlob() method that creates a binary-large object (BLOB) from the current image data of the canvas. This method takes a callback receiving the generated blob as a parameter. It also allows specifying the target format and image quality. Unless specified, the target format is PNG.

					canvas.toBlob(blob => {
  /* do something with the blob */

Copying Images to the Clipboard

The blob can now be written to the clipboard. The Async Clipboard API offers the writeText() and write() methods on the navigator’s clipboard object to copy data. While the first method is a convenient method to write plain text to the clipboard, the second one can be used to copy arbitrary data—as long as the browser or platform supports it. Currently, Safari only supports plain text, HTML, URI lists, and PNG data.

The write() method takes an array of clipboard items. They are created by calling the ClipboardItem() constructor that takes an object containing one or more representations of the item, with the representation’s MIME type as a key. The write() method returns a promise that resolves when copying was successful and rejects if it wasn’t. For security reasons, you may only invoke this action as a part of a user gesture (i.e., a keypress or a click). Depending on the browser, the user may need to give their permission first.

					canvas.toBlob(async (blob) => {
  await navigator.clipboard.write([
    new ClipboardItem({ [blob.type]: blob })

That’s all. With the help of Async Clipboard API, you can now copy your drawing to another application—for example, the macOS app Preview.

Pasting Images from the Clipboard

Let’s say you edited the image in the other application and want to paste the result back to the application, or you took a screenshot and want to edit it in Paint. In this case, the read() method of the Async Clipboard API is used. Again, the API also provides a readText() method for convenience. Reading back the data is a little more complicated, as you need to iterate over the list of clipboard items and their representations and pick the ones that match. Again, you may only invoke this action as a part of a user gesture, the user may be asked to allow reading from the clipboard, and the supported formats vary between browsers.

The following sample iterates over the items in the clipboard and their types. Whenever it finds a clipboard item with the image/png MIME type, it retrieves the blob data by calling the getType() method on the clipboard item.

					const items = await;
for (const item of items) {
    try {
      for (const type of image.types) {
          if (type === 'image/png') {
              const blob = await item.getType(type);
              /* draw image data from blob */
    } catch (err) {

Blob to Image

In order to draw the image to the canvas, its pixel data must first be extracted from the exchange format in the blob. This is where the Image() constructor comes into play. It creates a new instance of the HTMLImageElement (<img>). This can be used to load an image from a URL. The canvas API provides a drawImage() method that takes an HTMLImageElement as a parameter.

However, as the image was pasted from the clipboard, there’s no URL that could be used as the image source. Fortunately, with URL.createObjectUrl(), the web provides a method to create temporary URLs that point to a blob in memory. After the image was successfully loaded, the temporary URL is revoked to prevent memory leaks, and the image data can be drawn on the canvas using the drawImage() method.

					const image = new Image();
image.onload = () => {
  context.drawImage(image, 0, 0);
image.src = URL.createObjectURL(blob);

Et voilà! You can now paste images from other applications back to the Paint clone. In the sample application, this can either be done by pressing Ctrl+C/Ctrl+V, or by selecting the copy and paste entries in the application menu.

In the next part of this series, I want to show you how to save the drawings to the local file system and how to read them back. Furthermore, the Paint clone will be registered as a file handler for PNG files.


Current articles, screencasts and interviews by our experts

Don’t miss any content on Angular, .NET Core, Blazor, Azure, and Kubernetes and sign up for our free monthly dev newsletter.

EN Newsletter Anmeldung (#7)
Related Articles
.NET 8 brings Native AOT to ASP.NET Core, but many frameworks and libraries rely on unbound reflection internally and thus cannot support this scenario yet. This is true for ORMs, too: EF Core and Dapper will only bring full support for Native AOT in later releases. In this post, we will implement a database access layer with Sessions using the Humble Object pattern to get a similar developer experience. We will use Npgsql as a plain ADO.NET provider targeting PostgreSQL.
Originally introduced in .NET 7, Native AOT can be used with ASP.NET Core in the upcoming .NET 8 release. In this post, we look at the benefits and drawbacks from a general perspective and perform measurements to quantify the improvements on different platforms.
.NET 8 introduces a new Garbage Collector feature called DATAS for Server GC mode - let's make some benchmarks and check how it fits into the big picture.