# canvas

> A fixed 2D drawing of text, an emoji and blended shapes, read back as pixels and reported as a digest.

Source: https://whiskerjs.com/signals/canvas/ (last updated 2026-10-26)

**canvas at a glance (from the library code, signalsVersion 9)**

| Field | Value |
| --- | --- |
| Component name | `canvas` |
| In the visitorId | yes |
| structuredId group | [gpu (graphics)](https://whiskerjs.com/docs/identifiers/#structuredid) |
| Hashing | always a digest (first 128 bits of SHA-256) |
| Excluded under | Brave (brave_farbling); Firefox Fingerprinting Protection (firefox_fpp); Firefox resistFingerprinting (firefox_rfp); Tor Browser and Mullvad Browser (rfp_hardened); Safari Advanced Fingerprinting Protection (safari_afp) |
| Inner timeout | 1000 ms |
| Added in | [signalsVersion 1](https://whiskerjs.com/whats-new/) |

> **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 pixels are then read back with `getImageData(0, 0, 260, 80)`, which returns the red, green, blue and alpha byte of every pixel. Those bytes are what gets hashed.

Until signalsVersion 8 the scene was exported with `toDataURL()` instead. Firefox randomises that export per session while `getImageData()` stays exact (seen in Firefox 155 by the library’s real-browser tests), so stock Firefox gave a new id in every new browser context. Since signalsVersion 9 the pixels are read directly. For the [previous ids](https://whiskerjs.com/docs/identifiers/#previous-ids-linking-across-a-version-bump) the collector also takes one `toDataURL()` of the first drawing and records its digest as the value signalsVersion 8 would have reported.

## How the technique works

1. Draw the scene on a fresh canvas and read its pixels.
2. Draw exactly the same scene again on a second fresh canvas and read its pixels.
3. If either read gives no pixels, or every pixel is fully transparent, the browser gave back an empty or fake image: the component is reported `blocked` with ruleId `empty_image`.
4. If the two reads differ, something added random noise between them (per-read noise): the component is reported `noisy` with ruleId `render_mismatch` and carries no value.
5. If they agree, WhiskerJS checks the shared canvas [known-answer probe](https://whiskerjs.com/signals/noise-profile/): a 16x16 canvas filled with two known colours, read back twice. If that probe came back wrong (`seeded` or `per_read`), the browser adds noise that the double render cannot see, because it is the same on both reads within a session. The component is reported `noisy` with ruleId `seeded_noise`.
6. Otherwise the pixels are passed to the runner, which hashes them 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](https://whiskerjs.com/glossary/#structured-id) 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, a privacy setting or an extension that adds noise (the component turns `noisy` or `excluded`; that state is then the same in every session, so the id changes once, when the noise is switched on or off);
- the move to signalsVersion 9, which reads pixels instead of the exported image and so gives every browser a new digest once.

When only the `gpu` part of two structuredIds differs, a graphics change like these is a likely cause. See [What changes the id](https://whiskerjs.com/docs/identifiers/#what-changes-the-id).

## Browser support and what blocks it

- All current browsers offer the 2D canvas, so `canvas` is usually `ok` on [Chrome](https://whiskerjs.com/browsers/chrome-desktop/), [Edge](https://whiskerjs.com/browsers/edge/), [Safari](https://whiskerjs.com/browsers/safari-macos/), [Firefox](https://whiskerjs.com/browsers/firefox/) and mobile browsers.
- [Brave](https://whiskerjs.com/browsers/brave/): `excluded` with ruleId `brave_farbling`, because Brave randomises canvas reads per site and session.
- [Firefox with Fingerprinting Protection](https://whiskerjs.com/browsers/firefox/): `excluded` with ruleId `firefox_fpp`, because that protection randomises canvas readback per site and session.
- [Firefox with resistFingerprinting, Tor Browser and Mullvad Browser](https://whiskerjs.com/browsers/firefox-rfp-and-tor/): `excluded` with ruleId `firefox_rfp` (or `rfp_hardened` for Tor and Mullvad), because that mode blocks or fakes canvas readback.
- [Safari with Advanced Fingerprinting Protection](https://whiskerjs.com/browsers/safari-macos/): `excluded` with ruleId `safari_afp`, because that protection adds noise to canvas readback.
- Browsers or extensions that add noise on every read give `noisy` / `render_mismatch`; noise seeded per session that no exclusion row covers gives `noisy` / `seeded_noise`.

## 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 pixels and the known-answer probe was exact; the value is a digest (`hashed: true`) |
| `noisy` | `render_mismatch` | the two drawings differed |
| `noisy` | `seeded_noise` | the drawings agreed, but the canvas known-answer probe came back wrong |
| `blocked` | `empty_image` | no pixels could be read, or every pixel was transparent |
| `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_fpp`, `firefox_rfp`, `rfp_hardened`, `safari_afp`, `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`](https://whiskerjs.com/signals/canvas-readback/) and [`noise_profile`](https://whiskerjs.com/signals/noise-profile/) components tell you whether canvas reads are being altered at all, and how.

## Example value

Illustrative (the digest differs between setups, and every digest changed at signalsVersion 9):

```json
"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.

### What does noisy with seeded_noise mean?

The two drawings agreed, but the canvas known-answer probe (a small canvas in two known colours) came back wrong. The noise is seeded: the same on every read in one session and different in the next, as Brave, Firefox Fingerprinting Protection and Safari Advanced Fingerprinting Protection do. The value is left out so the id does not change every session.

### Why did my canvas digest change at signalsVersion 9?

Until signalsVersion 8 the scene was exported with toDataURL(), which Firefox randomises per session, so stock Firefox got a new id in every new browser context. Since signalsVersion 9 the pixels are read with getImageData(), which stays exact. The digest is new for every browser, so the gpu part of the structuredId changes once.

### Is the image sent to my server?

No. The pixels are hashed in the browser and only the first 128 bits of their SHA-256 digest leave the library, as 32 hex characters.

## Related pages

- [webgl_image](https://whiskerjs.com/signals/webgl-image/): The WebGL drawing, also rendered twice and checked for noise.
- [canvas_readback](https://whiskerjs.com/signals/canvas-readback/): The canvas known-answer probe: are canvas reads exact or altered.
- [noise_profile](https://whiskerjs.com/signals/noise-profile/): Which known-answer probes saw seeded or per-read noise.
- [fonts_canvas](https://whiskerjs.com/signals/fonts-canvas/): Font detection with canvas text measurement.
- [Canvas fingerprinting](https://whiskerjs.com/glossary/#canvas-fingerprinting): The technique in one definition.
- [The double render and the known answers](https://whiskerjs.com/docs/how-it-works/#noise-handling-the-double-render-and-the-known-answers): How WhiskerJS spots canvas, WebGL and audio noise.
- [Brave](https://whiskerjs.com/browsers/brave/): Why canvas is excluded there.
