JMM’s notes on

Using Images from Web APIs

HTMLImageElement, Canvas, inputs, Files, Blobs

File inputs

You can accept files with an input like so:

<input type="file" id="myfiles" name="files" accept="image/*" multiple="multiple"/>

If you want an image from the camera:

<input type="file" id="myfiles" name="files" accept="image/*" capture="environment" multiple="multiple"/>

Selecting files triggers a “change” event. To get the files in an event handler, get the element (either as a target of the event or with a selector) and access the “files” property:

document.querySelector("#myfiles").files

This returns a FileList, which you can use like an ECMAScript Array. Each element of the FileList is a File object, which is a type of Blob.

File to HTMLImageElement

(This also works with Blobs, which are like Files.)

Once you have a file, like

const myfile = document.querySelector("#myfiles").files[0];

You can use it as an HTMLImageElement by setting the src to a Blob URL like so:

const img1 = document.createElement("img");
img1.src = URL.createObjectURL(myfile);

URL to HTMLImageElement

Useful for drawing an SVG (or other images) to a canvas.

async function loadImage(url) {
    return await new Promise((resolve, reject) => {
	const image = new Image();	// You could also specify a size like Image(200,200);
	image.addEventListener("load", () => resolve(image));
	image.addEventListener("error", reject);
	image.crossOrigin = "use-credentials";
	image.alt = "";
	image.src = url;
    });
}

Fetch Image from URL

If you wanted to get info like the MIME type, you could fetch the resource yourself like:

async function fetchImage(url) {
    const response = await fetch(url, { mode: 'cors' })
    if (!response.ok) {
	throw new Error(`Fetching failed with status: ${response.status}`);
    }

    const blob = await response.blob();
    // If you needed the MIME/Media Type you could look at blob.type here.

    return await new Promise((resolve, reject) => {
	const image = new Image();
	image.addEventListener("load", () => resolve(image));
	image.addEventListener("error", reject);
	image.src = URL.createObjectURL(blob);
    });
}

Getting Image Dimensions

With the previous img1 element use img1.naturalWidth and img1.naturalHeight to get dimensions. If you use img1.width or img1.height, you’ll get the rendered height, which can differ if you set the CSS “max-width: 100%” (for example).

Also note that you can’t immediately get “naturalWidth” after creating an “img” and setting the “.src”: it’ll initially return “0”. You need to check after the “load” event to read “naturalWidth” and “naturalHeight”.

HTMLImageElement to Canvas

To take that img1 and draw it on a canvas:

const canvas1 = document.createElement("canvas");
document.body.appendChild(canvas1);
canvas1.width = img1.naturalWidth;
canvas1.height = img1.naturalHeight;
const ctx1 = canvas1.getContext("2d");
ctx1.drawImage(img1, 0, 0);

Note that you’ll probably want to scale it down, as canvas elements have max dimensions.

Downloading a Canvas element

To save a canvas, I’ll get a Blob from the canvas, then make a link where

Canvas to Blob

Here’s how to get a WebP blob of the canvas:

const blob1 = await new Promise(resolve => { canvas1.toBlob(resolve, "image/webp", 0.80) }); 

where 0.80 is the quality of the WebP.

If you have an OffscreenCanvas instead, use “.convertToBlob()” like so:

const blob1 = await canvas1.convertToBlob({type: "image/webp", quality: 0.80});

Downloading the blob

Here’s how to make a link to download that blob:

const a1 = document.createElement("a");
a1.href = URL.createObjectURL(blob1);
a1.download = "example.webp";
a1.textContent = "Download the blob";
document.body.appendChild(a1);

Note that you can actually just click it programmatically to trigger a download like so:

a1.click();

Then you’ll probably want to clean up that element:

URL.revokeObjectURL(a1.href);
a1.remove();

You may actually be able to release the blob URL earlier according to an MDN article on Using files from web applications.

SVG to Blob

Like, I originally wanted to be able to rasterize an SVG to a canvas element.

While you can use an SVG-in-an-image-element and get that as a blob, I think you probably can’t directly rasterize an SVG-element-embedded-in-HTML. This is prevented, I believe, for security reasons. While SVG in <img> elements can’t access network external resources, embedded SVGs can. Like, an SVG could have a <foreignObject> with an <iframe> pointing to some cross-origin resource. Being able to rasterize that would be a security flaw.

Probably your best bet would be to convert as much as you can to data URIs and have that used as an image element, and try to rasterize that.

Here’s some code that will convert an SVG-as-<img> to a blob using an OffscreenCanvas:

async function imgToBlob(img1, scale) {
    scale ??= 1;
    const dWidth  = img1.naturalWidth  * scale;
    const dHeight = img1.naturalHeight * scale;
    const canvas1 = new OffscreenCanvas(dWidth, dHeight);
    const ctx1 = canvas1.getContext("2d");
    ctx1.drawImage(img1, 0, 0, dWidth, dHeight);
    const blob1 = await canvas1.convertToBlob({type: "image/png"});
    return blob1;
}

SVG to File

To convert an SVG element to a File, we’ll serialize the XML as a string, then encode the string to a Uint8Array which we can use to construct a File.

Assume you have an SVG element named “svgel”:

function svgToFile(svgel, filename) {
    let str = new XMLSerializer().serializeToString(svgel);
    let uintarray = new TextEncoder().encode(str);
    return new File([uintarray], filename, { type: "image/svg+xml" });
}

You can then use this File to create an HTMLImageElement as described previously.

Uploading blobs

If you wanted to modify the original <input type="file"> element, you’ll need to set its “files” property. However, you can’t directly modify a FileList object, nor does FileList appear to have a constructor. Instead, we’ll make a DataTransfer object and convert our Blob to a File, then we’ll set the input’s “files” property using our DataTransfer object’s “files” property.

Blobs to Files

Converting a Blob to a File is fairly straightforward. You just need to provide a file name and optionally a media (MIME) type:

const f2 = new File([blob1], "somefilename.webp", { type: "image/webp" });

Modifying <input type="file">

Now that we have a file, we’ll make a DataTransfer object and use that to set the <input type="file">’s “files” property:

const dt1 = new DataTransfer();
dt1.items.add(f2);
document.querySelector("#myfiles").files = dt1.files;

These tools should let you take in an image and modify it before uploading. Some reasons to do this would be to resize, compress, or strip EXIF metadata before uploading.

Making src use a data: URI

The following function takes in an HTMLImageElement and makes the src use a data: URI. You might want to do this to convert images in an SVG or for saving an HTML file, though I haven’t tested this for either of those purposes yet.

async function imageUseDataURI(image) {
    const url = image.currentSrc;
    const response = await fetch(url, { mode: 'cors' })
    if (!response.ok) {
	throw new Error(`Fetching failed with status: ${response.status}`);
    }
    const blob = await response.blob();
    // Load blob to data URI.
    const datauri = await new Promise((resolve, reject) => {
	const freader = new FileReader();
	freader.addEventListener("load", () => {
	    resolve(freader.result);
	});
	freader.addEventListener("error", reject);
	freader.readAsDataURL(blob);
    });
    // I guess wait for it to load.
    return await new Promise((resolve, reject) => {
	image.addEventListener("load", () => resolve(image));
	image.addEventListener("error", reject);
	image.src = datauri;
    });
}

Miscellaneous notes

To-dos