Signal · signalsVersion 5

canvas

A fixed 2D drawing of text, an emoji and blended shapes, exported as an image and reported as a digest.

canvas at a glance (from the library code, signalsVersion 5)
Component namecanvas
In the visitorIdyes
structuredId groupgpu (graphics)
Hashingalways a digest (first 128 bits of SHA-256)
Excluded underBrave (brave_farbling); Firefox resistFingerprinting and Tor Browser (firefox_rfp)
Inner timeout1000 ms
Added insignalsVersion 1
In plain words

WhiskerJS draws a small, fixed picture in an invisible canvas: two lines of text with a cat emoji, three overlapping coloured circles and a ring. Every device draws text, emoji and blended colours very slightly differently, so the exact pixels of the picture differ between systems. The picture is never shown and never sent: WhiskerJS turns it into a short code (a digest) in the browser.

What it reads

The canvas component creates a detached <canvas> element, takes its 2D context with getContext('2d'), sets it to 260 by 80 pixels and draws one fixed scene:

  • an orange filled rectangle;
  • the text “Whiskers twitch, jumpy fox” followed by a cat face emoji, once in 14 px “Times New Roman” and once in 19 px Arial with a semi-transparent green fill;
  • three circles drawn with the multiply composite operation, so their overlaps blend;
  • a ring made of two arcs filled with the evenodd rule.

The canvas is then exported with toDataURL(), which returns the image as a PNG data URL. That string is what gets hashed.

How the technique works

  1. Draw the scene once and export it.
  2. Draw exactly the same scene again on the same canvas and export it again.
  3. If the first export does not start with data:image/ or is suspiciously short, the browser gave back an empty or fake image: the component is reported blocked with ruleId empty_image.
  4. If the two exports differ, something added random noise between the two reads: the component is reported noisy with ruleId render_mismatch and carries no value.
  5. Otherwise the export is passed to the runner, which hashes it with SHA-256 (canvas is one of the “large” components that are always hashed) and keeps the first 128 bits.

The element is never attached to the page, so nothing appears on screen.

Why it helps

Turning text and shapes into pixels involves many layers that differ between systems: the font files installed and their versions, the font rasteriser and its anti-aliasing and hinting, the emoji font (each operating system ships its own emoji images), the graphics library that blends colours, and sometimes the GPU. Two devices with the same browser can still produce different pixels, while the same device usually produces the same pixels every time. That makes the digest useful for telling setups apart. It sits in the gpu group of the structuredId together with the WebGL and WebGPU components.

Stability

The digest stays the same as long as the drawing pipeline stays the same. It can change after:

  • a browser update that changes text rendering or the graphics back end;
  • an operating system update that changes fonts or the emoji set;
  • a GPU driver update, or switching hardware acceleration on or off;
  • installing or removing the fonts the scene uses;
  • a private window or an extension that adds noise (the component turns noisy, which also changes the visitorId compared with an ok run).

When only the gpu part of two structuredIds differs, a graphics change like these is a likely cause. See What changes the id.

Browser support and what blocks it

  • All current browsers offer the 2D canvas, so canvas is usually ok on Chrome, Edge, Safari, Firefox and mobile browsers.
  • Brave: excluded with ruleId brave_farbling, because Brave randomises canvas reads per site and session.
  • Firefox with resistFingerprinting and Tor Browser: excluded with ruleId firefox_rfp, because that mode blocks or fakes canvas readback.
  • Browsers or extensions that add noise only sometimes (for example in private windows) give noisy / render_mismatch.

Privacy

The image itself contains only the fixed test scene, nothing from the page or the user. It reveals details about the rendering stack (fonts, emoji set, graphics), not personal content. It leaves the library only as a 32-character digest. Drawing on a canvas needs no permission, shows no prompt and makes no network request.

How WhiskerJS handles noise and failures

Status ruleId When
ok none the scene was drawn twice with identical output; the value is a digest (hashed: true)
noisy render_mismatch the two exports differed
blocked empty_image the export was empty or not an image
blocked security_error, threw the browser threw while drawing or exporting
unavailable no_document, no_api, no_context no document, no getContext, or no 2D context
unavailable no_digest Web Crypto is missing, so the value cannot be hashed and is not sent raw
excluded brave_farbling, firefox_rfp, caller_excluded the exclusion table or your exclude list
timed_out timeout, aborted the 1000 ms inner timeout, the overall budget or your abort signal

Any status other than ok adds the same placeholder line to the visitorId, so blocked in one run and timed_out in the next give the same id. The separate canvas_readback probe tells you whether canvas reads are being altered at all.

Example value

From a headless Chromium test browser in a container:

"canvas": { "status": "ok", "value": "ddf67a6b33259ceea15174ae31a4ac73", "hashed": true }

Questions and answers

Why does canvas show as excluded in Brave?

Brave adds per-site, per-session noise to canvas reads (it calls this farbling). Reading it would only add noise to the id, so the exclusion table skips canvas for the brave browser class and reports excluded with ruleId brave_farbling.

What does noisy with render_mismatch mean?

WhiskerJS drew the same scene twice and the two images differed. That only happens when the browser or an extension adds random noise to canvas reads, so the value is left out and the component counts as missing in the visitorId.

Is the image sent to my server?

No. The exported image is hashed in the browser and only the first 128 bits of its SHA-256 digest leave the library, as 32 hex characters.