deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

canvas.toBlob silently returns PNG when a browser can't encode your format

The HTML spec lets browsers hand back a PNG when canvas.toBlob can't encode the requested format, with no error. A dev.to post shows how to detect it.

canvas.toBlob silently returns PNG when a browser can't encode your format

A silent substitution, not a bug

A post on dev.to describes a bug hunt that began with a user report: .webp files exported from a browser-based image tool would not open on the desktop. A hex editor explained why — the first bytes of the file were PNG's magic number, not WebP's, despite the .webp extension. The export code had asked canvas.toBlob for image/webp, and on the surface everything worked: the callback fired, the blob was non-null, and its size looked plausible.

The spec requires the fallback

As the post explains, this behavior is mandated, not accidental. The HTML specification says that when a user agent cannot produce the requested image type, it must emit a PNG instead — without throwing, logging, or signalling the substitution anywhere except blob.type, which will read image/png. The author notes that iOS versions below 16.4 respond to an image/webp request this way. toDataURL follows the same rule, but there the swap is at least visible, because the resulting string starts with data:image/png.

There is no way to ask up front

Media APIs let developers query support — MediaRecorder.isTypeSupported and navigator.mediaCapabilities.encodingInfo both exist. Canvas encoding has no equivalent: neither toBlob nor toDataURL exposes a capability check, and the spec defines the fallback without offering a way to detect it beforehand.

The post also rules out user-agent sniffing. Every browser on iOS is WebKit underneath, embedded webviews track system versions differently from the standalone browser, and the 'Request Desktop Website' setting makes an iPhone present a macOS user agent. A UA string identifies a browser; it does not report what its encoder can do at that moment.

Probe the encoder and compare types

The workaround the author settled on: draw a tiny canvas, ask it to encode, and check what actually comes back. The key points:

  • Use a 2×2 canvas rather than 1×1 — some encoders take special paths for degenerate sizes, so a 1×1 result may not be representative.
  • Fill with a semi-transparent color so the probe also exercises the alpha channel.
  • The verdict is whether blob.type matches the requested MIME; a non-null blob alone proves nothing.
  • Cache the result per MIME type per session, and run the probe during idle time — for example after a file is selected, not on first paint or on the convert click.

Running this on desktop Chromium, the author found image/webp and image/jpeg encode normally, while requests for image/avif, image/heic, image/tiff and image/gif each returned byte-for-byte identical 95-byte PNGs. A null check alone would have marked all six as supported. The AVIF row is the notable one: desktop Chrome decodes AVIF without difficulty but will not encode it, so this is not a mobile-only problem.

Decoding needs its own probe

Encoding and decoding are separate code paths, the post points out — Safari 16 can decode AVIF without being able to encode it, and nothing mainstream encodes HEIC. Testing decode support requires a sample file in that format, which the author suggests inlining as tiny base64 data URLs, since a parseable 1×1 AVIF costs only a couple of hundred bytes. Fetch the sample and call createImageBitmap: it either resolves or throws. The Image element with onload/onerror is a poorer fit, because some browsers fire onload with a naturalWidth of zero for files they cannot decode.

When support is missing, say so

The post closes with guidance for the failure case: swap to an equivalent format and disclose it, load a WASM codec such as libwebp, libavif or libheif on demand (which sends code down to the device while images stay local), or fall back to a server — but state which path is being taken before the user clicks. HEIC output remains the unsolved case: HEVC licensing and encoder size push it server-side. The author, who builds a browser-based image tool called imging.cn, is still looking for a workable client-side HEVC encoder.

Why it matters

This is a rare class of failure: spec-compliant, silent, and invisible to a null check. Any tool that exports, converts, or compresses images client-side can ship this bug while its own tests pass, because the developer's browser supports the format being requested. The defense is one comparison — the returned blob's type against the requested MIME — plus honest handling when the answer is no. The post doubles as a practical checklist: probe encoding, probe decoding separately, cache the results, and never substitute formats without telling the user.

  • #canvas
  • #webp
  • #javascript
  • #image-encoding
  • #browser-compatibility

Related posts