Practical Uses of Object URLs

At PSPDFKit, we always try to leverage every technology available to us to improve the quality and performance of our products.

As an example of this, PSPDFKit for Web introduces new web API features as soon as they’re available in the browsers we support. In the case of object URLs, they’ve been around for a long time, so we’re pretty familiar with them.

Object URLs provide a way to reference File or Blob objects through a browser-generated string that can be used as a URL placeholder in several web elements — such as images, videos, iframes, and web workers. This blog post will share a handful of ways you can use object URLs to accomplish a variety of tasks.

Rendering a Generated Image

One popular application of object URLs is to convert a canvas to an image so it can be reused in different places without having to draw it over and over again. Let’s say we generate an image in a canvas either from user input or, for simplicity’s sake, from a simple algorithm like this one:

// Generate a canvas image.
const width = 64;
const height = 64;
const canvas = document.createElement('canvas');
canvas.width = width * window.devicePixelRatio;
canvas.height = height * window.devicePixelRatio;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(canvas.width, canvas.height);
for (let i = 0; i < imageData.data.length; i += 4) {
	imageData.data[i] = 255;
	imageData.data[i + 1] = (i + 77) % 255;
	imageData.data[i + 2] = (i + 191) % 255;
	imageData.data[i + 3] = (i + 33) % 255;
}
ctx.putImageData(imageData, 0, 0);
document.body.appendChild(canvas);

Now we want to show the generated image in an img element instead of a canvas. So we convert the canvas to a Blob, create an object URL for it, and use it as a source for our image:

// This would look nicer if `Canvas.toBlob()` returned a `Promise` object.
const canvasBlob = await new Promise((resolve) => canvas.toBlob(resolve));
const canvasBlobUrl = URL.createObjectURL(canvasBlob);
const img = new Image();
img.onload = () => {
	document.body.appendChild(img);
	URL.revokeObjectURL(canvasBlobUrl);
};
img.src = canvasBlobUrl;

Now this Image instance can be rendered with the image from the canvas element. However, object URLs come with a caveat: Unlike other JavaScript objects, we need to manually release them so that the object they point to can be dereferenced and garbage collected by the browser. We do this explicitly using URL.revokeObjectURL(), as seen above.

Remember to append the image to the DOM before calling URL.revokeObjectURL(), or the browser may not be able to render it!

Creating a Downloadable File

Some web applications generate content directly in the browser, without requesting it from a server, and they need to make it downloadable, or even trigger a download automatically.

Object URLs can also help with this task:

const blob = new Blob(['Some content for a downloadable text file.'], {
	type: 'text/plain',
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.style.display = 'none';
a.download = 'download.txt';
a.setAttribute('download', 'download.txt');
document.body.append(a);
a.click();
a.remove();
URL.revokeObjectURL(url);

The snippet above creates an object URL pointing to a text file Blob, which is then downloaded to the user’s local file system.

Spawning an Inline Web Worker

We can also use object URLs as a source for web workers: This technique allows us to spawn web workers without needing to add a separate script file for them.

The only complication is that the actual web worker code needs to be passed as a string to the Blob constructor in order to be able to convert it to a Blob:

<script id="myWorker" type="javascript/worker">
	self.onmessage = function(e) {
	  console.log(`[WORKER] Message received: ${e.data}`);
	  self.postMessage("Hello back from the worker thread!");
	};
</script>
<script type="application/javascript">
	const workerBody = document.querySelector('#myWorker').textContent;
	const workerBlob = new Blob([workerBody], {
		type: 'application/javascript',
	});
	const workerUrl = URL.createObjectURL(workerBlob);
	const worker = new Worker(workerUrl);
	worker.onmessage = function (e) {
		console.log(`[MAIN] Message received: ${e.data}`);
	};
	worker.postMessage('Hello from the main thread.');
	URL.revokeObjectURL(workerUrl);
</script>

The snippet above spawns a worker thread from a script that has been instantiated at runtime, without the need to further communicate with the server or request additional files from it.

Rendering an Image Generated in an Inline Web Worker

What about combining some of these applications of object URLs, if only just for fun? Let’s create an image in an inline web worker and pass it to the main thread for rendering! Yes, you can create images in a worker (but you cannot append them to the DOM from there):

<script id="myWorker" type="javascript/worker">
	self.onmessage = function(e) {
	  console.log(`[WORKER] Message received: ${e.data}`);
	  const width = 300;
	  const height = 200;
	  const canvas = new OffscreenCanvas(width, height);
	  const imageArray = new Uint8ClampedArray(width * height * 4);
	  for (let i = 0; i < imageArray.length; i += 4) {
	    imageArray[i] = Math.floor(Math.random() * 255);
	    imageArray[i + 1] = Math.floor(Math.random() * 255);
	    imageArray[i + 2] = Math.floor(Math.random() * 255);
	    imageArray[i + 3] = 255;  // A value
	  }
	  const imageData = new ImageData(imageArray, width, height);
	  const ctx = canvas.getContext("2d");
	  ctx.putImageData(imageData, 0, 0);
	  canvas.convertToBlob({ type: "image/png" }).then(blob => {
	    const url = self.URL.createObjectURL(blob);
	    self.postMessage(url);
	  });
	};
</script>
<script type="application/javascript">
	const workerBody = document.querySelector('#myWorker').textContent;
	const workerBlob = new Blob([workerBody], {
		type: 'application/javascript',
	});
	const workerUrl = URL.createObjectURL(workerBlob);
	const worker = new Worker(workerUrl);
	worker.onmessage = function (e) {
		const img = new Image();
		const url = e.data;
		img.onload = () => {
			document.body.append(img);
			URL.revokeObjectURL(url);
		};
		img.src = url;
		console.log('[MAIN] Message received');
	};
	worker.postMessage('Hello from the main thread.');
	URL.revokeObjectURL(workerUrl);
</script>

What happened here? In the worker, we filled a canvas with random color values for every pixel, and then, instead of transferring the canvas or a bitmap ArrayBuffer back to the main thread, we:

Object URLs can be shared across workers without needing to pass any object around: The Blob can just be read from the main thread, and its object URL revoked there.

Rendering an Image from an Inline Web Worker in a Generated Iframe

We can even use object URLs to populate an iframe without needing to reference external resources.

In the following snippet, we show an example of this implementation: A web worker is launched from an inline-created iframe, and it sends back the object URL of a generated image.

Clicking on the Download once button, you can download the image — but only once, as the object URL is revoked immediately after:

<!-- #myIframeContent contains the content to be used for the generated iframe -->
<span id="myIframeContent">
	<base href="https://www.example.com/" />

	<!-- Code for the generated Web Worker -->
	<script id="myWorker" type="javascript/worker">
		self.onmessage = function(e) {
		  console.log(`[WORKER] Message received: ${e.data}`);
		  const width = 300;
		  const height = 200;
		  const canvas = new OffscreenCanvas(width, height);
		  const imageArray = new Uint8ClampedArray(width * height * 4);
		  for (let i = 0; i < imageArray.length; i += 4) {
		    imageArray[i] = Math.floor(Math.random() * 255);
		    imageArray[i + 1] = Math.floor(Math.random() * 255);
		    imageArray[i + 2] = Math.floor(Math.random() * 255);
		    imageArray[i + 3] = 255;  // A value
		  }
		  const imageData = new ImageData(imageArray, width, height);
		  const ctx = canvas.getContext("2d");
		  ctx.putImageData(imageData, 0, 0);
		  canvas.convertToBlob({ type: "image/png" }).then(blob => {
		    const url = self.URL.createObjectURL(blob);
		    self.postMessage(url);
		  });
		};
	</script>

	<!-- Iframe's main thread code -->
	<script type="application/javascript">
		const workerBody = document.querySelector('#myWorker').textContent;
		const workerBlob = new Blob([workerBody], {
			type: 'application/javascript',
		});
		const workerUrl = URL.createObjectURL(workerBlob);
		const worker = new Worker(workerUrl);
		worker.onmessage = function (e) {
			const img = new Image();
			img.src = e.data;
			document.body.append(img);
			// We do not revoke the object URL yet, as otherwise we won't be able to download the image.
			console.log('[MAIN] Message received');
		};
		worker.postMessage('Hello from the main thread.');
		URL.revokeObjectURL(workerUrl);
		async function download() {
			const a = document.createElement('a');
			a.href = document.querySelector('img').src;
			a.style.display = 'none';
			a.download = 'image.png';
			a.setAttribute('download', 'image.png');
			document.body.append(a);
			a.click();
			a.remove();
			URL.revokeObjectURL(document.querySelector('img').src);
			document.querySelector('button').setAttribute('disabled', true);
		}
	</script>
	<button onclick="download();">Download</button>
	<br />
</span>

<!-- Our main window code follows -->
<script type="application/javascript">
	// We need to set the iframe's base URI to prevent incurring cross-origin issues, as the iframe's
	// origin will be set to the "blob:"-prefixed URL's origin by default.
	const iframeBody = document
		.querySelector('#myWorker')
		.textContent.replace(
			'https://www.example.com/',
			`blob:https://${location.hostname}`,
		);
	const iframeBlob = new Blob([iframeBody], { type: 'text/html' });
	const iframeUrl = URL.createObjectURL(iframeBlob);
	const iframe = document.createElement('iframe');
	iframe.width = '500';
	iframe.height = '400';
	iframe.src = iframeUrl;
	document.append(iframe);
	URL.revokeObjectURL(iframeUrl);
</script>

You can play with this example code in this CodePen.

Conclusion

As you see, object URLs allow us to perform some tricks that would otherwise be more difficult, while at the same time making code less verbose. That’s one of the reasons we adopted them long ago at PSPDFKit for Web, and we keep finding uses for them.