Skip to content

Build1 publisher3 min readPublished

The canvas spec tells browsers to answer an AVIF export request with a PNG

No major browser encodes AVIF from a canvas, and the spec says to hand back a PNG without throwing. The only support test that works is reading blob.type on the blob you get back.

The Engineer · Build desk

Illustration accompanying The canvas spec tells browsers to answer an AVIF export request with a PNG

What happened

  • The canvas spec directs a browser that cannot encode the requested type to return a PNG instead, with no exception thrown and no promise rejected, so all you get is a changed blob.type.
  • The image tool Filewhisk checked blob.type before offering a download, so its users saw a "not supported" message while two pages went on advertising JPG and PNG conversion to AVIF.
  • The working export ships Squoosh's libavif encoder as @jsquash/avif under Apache-2.0, running in the browser instead of on a server.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • decision Any application that uses canvas to produce something other than PNG needs a one-time probe at startup, and the answer has to come from the type of a blob the browser actually produced.
  • exposure Where the download filename is built from the requested mime type, users are holding files whose extension misdescribes the bytes, and the breakage surfaces in whatever tool they open them with next.
  • constraint Cross-origin isolation and third-party ad tags cannot share a page, so an ad-funded site encodes AVIF single-threaded, at seconds per image.
  • cost Skipping the bundler means the compiled codec is vendored under a version-stamped path, so the upgrade is a manual copy rather than a package bump.

Call toBlob with a type the browser cannot encode and nothing fails. The callback fires, you get a Blob, and blob.type reads image/png where you asked for image/avif [1]. The dev.to post documenting this measured both entry points on Chrome 153, canvas.toBlob and OffscreenCanvas.convertToBlob, and both logged image/png [2]. Chromium has an open issue for it, 40848792, and neither Firefox nor Safari encodes AVIF from a canvas [3].

Support therefore has to be read off the output. The post's canEncode() makes a 1x1 canvas, asks for the mime type at quality 0.8, and resolves true only when blob.type comes back equal to the request [4]. WebP passes in Chromium browsers. AVIF passes nowhere today [5].

The mislabelled file is the application's doing. When download code builds a filename from the type it requested, the user gets a .avif name wrapped around PNG bytes [6]. Filewhisk, the author's set of browser-only image tools, checked blob.type before offering the download and showed a "not supported" message. Two pages were left promising a JPG and PNG conversion to AVIF they could not perform [7].

Chrome 85 added AVIF decoding, and that is where the belief came from. "Displaying a format and producing it are different features, and I had merged them in my head," the author wrote [8].

Server-side conversion was out. The author wrote that sending the image to a server was not an option because the site's promise to users is that "your files never leave your device" [15]. jSquash packages Squoosh's libavif encoder as @jsquash/avif under Apache-2.0 [9]. It ships in two builds. The faster one needs SharedArrayBuffer, so the page has to be cross-origin isolated with COOP and COEP, and those headers block cross-origin resources that do not opt in, which ad and analytics tags generally do not [10]. The author shipped the single-threaded build [10].

Going without a bundler cost one more detour. jSquash's encode.js imports a bare specifier from wasm-feature-detect to switch to the threaded build, and a bare specifier fails to load with no bundler and no import map, so the author skipped the wrapper and imported the compiled codec/enc/avif_enc.js, which loads its .wasm relative to import.meta.url [11]. The worker is served from /vendor/avif-2.1.1/ [16].

A 1600x719 photo encodes in about 1.3 to 1.7 seconds on the author's machine [12]. That is 1,150,400 pixels [1], so single-threaded throughput works out at roughly 0.7 to 0.9 megapixels per second [2]. On the main thread the page is frozen for the whole encode, so it runs in a module worker at defaults of quality 50 and speed 6 [12][13]. The RGBA pixels for that photo come to about 4.6 MB [3], and the page transfers the buffer to the worker instead of copying it [14].

For those seconds to mean anything on your site you need a comparable single core, the same pixel count, and the same settings, since speed 6 is a middle setting and quality 50 is not lossless [13]. The post does not identify the machine.

What to watch

  • Chromium issue 40848792 closing as fixed. That would flip canEncode's answer in Chrome and leave the rest of the app untouched.
  • Whether jSquash drops the bare specifier from encode.js so no-bundler sites can use the shipped detection path.
  • Whether Firefox or Safari ship a canvas AVIF encoder; the output-type test currently fails in all three engines.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories