Creating a canvas without "document" in JavaScriptPublished Fri May 17 2024, 3 min read

I apparently do a lot of weird shit when making websites. As such, I often encounter dumb issues nobody else has ever seen, and every search result for how to fix it states the same exact obvious answer that does not apply to my use case.

Tonight's scenario

I'm trying to write an API endpoint to return an image.

The problem

By default, Hashicon tries to generate a canvas programmatically using the way every website will tell you to declare a canvas programmatically:

document.createElement('canvas');

Because this is an API route that runs serverside, there is no document to render to.

ReferenceError: document is not defined
    at createCanvas (node_modules/@emeraldpay/hashicon/lib/utils.js:36:18)

The solution

Automattic/node-canvas allows you to create a canvas on a NodeJS server so you don't need to be in the browser. Hashicon allows you to set the createCanvas function it uses, so setting it to the node-canvas create function makes it work just fine.

I'm using TypeScript here, but I trust you're smart enough to convert it if you're using JavaScript 😊

// @ts-ignore
let icon: Canvas = hashicon(params.hash, { createCanvas: createCanvas });

By default, TypeScript complains about the node-canvas create function returning their own Canvas type since Hashicon is expecting a HTMLCanvasElement. Because TypeScript is still bound by the rules of JavaScript (or more accurately, lack thereof) and because Canvas is functionally identical to HTMLCanvasElement, it works fine and @ts-ignore gets TypeScript to shut the fuck up about it.

The main part of my use case that didn't work was the HTMLCanvasElement.toBlob() method; however, Canvas offers a toBuffer() method instead that can very easily be transformed into a Blob given the Mimetype.

// @ts-ignore
let blob = new Blob([icon.toBuffer("image/png")], { type: "image/png" });

You can view a full list of broken shit on the wiki.

Opening libuuid.so.1

I also ran into the following error when initially trying to run node-canvas:

Error: libuuid.so.1: cannot open shared object file: No such file or directory
    at Module._extensions..node (node:internal/modules/cjs/loader:1465:18)
    at Module.load (node:internal/modules/cjs/loader:1206:32)
    at Module._load (node:internal/modules/cjs/loader:1022:12)
    at Module.require (node:internal/modules/cjs/loader:1231:19)
    at require (node:internal/modules/helpers:179:18)
    at Object.<anonymous> (node_modules/canvas/lib/bindings.js:3:18)
    at Module._compile (node:internal/modules/cjs/loader:1369:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1427:10)
    at Module.load (node:internal/modules/cjs/loader:1206:32)
    at Module._load (node:internal/modules/cjs/loader:1022:12) {
  code: 'ERR_DLOPEN_FAILED'
}

Thanks to this issue, I was able to determine that libuuid had to be added to the LD_LIBRARY_PATH environment variable. Thank God Nix makes modifying this stuff easy.

devShells.default = pkgs.mkShell rec {

    packages = with pkgs; [
        yarn-berry
        libuuid
    ];

    # https://github.com/Automattic/node-canvas/issues/1947#issuecomment-991016228
    LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath packages;

};

Why not just use OffscreenCanvas?

OffscreenCanvas is a fairly new interface that allows you to render a canvas outside the DOM without needing any additional dependencies. Unfortunately, when MDN says "This feature is available in Web Workers," it means this feature is only available in Web Workers and not my RequestHandler.

Fin.

Hope this helps someone because it took me 2 hours to figure out and another hour to write this post about it. Enjoy.