FWC-1§4Normative

Pixel carrier

How container bytes map onto RGBA samples. Two modes share one invariant: alpha is always 255, so nothing is ever premultiplied away.

The alpha invariant

Browsers store canvas pixels premultiplied by alpha. A pixel with alpha 0 loses its colour entirely; a pixel with alpha 128 has its RGB values quantised on the round trip. This is the "colour drift" that makes naive image steganography unreliable across engines. FWC-1 removes the problem by construction: encoders MUST write 255 to every alpha sample, and decoders MUST ignore the alpha channel entirely. With alpha at 255 the premultiplication is the identity and getImageData returns exactly the bytes that were put in, on every engine tested (Blink, Gecko, WebKit).

Sample ordering

Both modes walk the flat pixel array from index 0 upward and skip every alpha sample (i % 4 === 3). The sequence of visited samples is therefore R₀ G₀ B₀ R₁ G₁ B₁ … — three colour samples per pixel, pixels in row-major order from the top-left corner. This ordering is identical for encoding and decoding and MUST NOT depend on image width beyond the row-major convention inherent in getImageData.

Mode `noise` — full-byte carrier

Each visited colour sample holds one whole container byte. Byte j of the container is written to the j-th visited sample. Capacity is therefore 3 · w · h − 12 bytes. Samples beyond the container MUST be filled with cryptographically random values (crypto.getRandomValues) so the image is uniformly noisy and the container boundary is not visible. Decoders MUST NOT rely on the tail being random; they read exactly 12 + length bytes and stop.

// write: data is the RGBA buffer, bytes is header ‖ body
for (let i = 0, j = 0; j < bytes.length; i++) {
  if (i % 4 === 3) continue        // never touch alpha
  data[i] = bytes[j++]
}

// read n bytes
for (let i = 0, j = 0; j < n; i++) {
  if (i % 4 === 3) continue
  out[j++] = data[i]
}
noise mode — write and read

Mode `lsb` — two-bit steganographic carrier

Each visited colour sample carries two bits in its least-significant positions; the upper six bits belong to the cover image and are preserved. Bits are consumed from each container byte most-significant first: byte b contributes the bit pairs (b >> 6) & 3, (b >> 4) & 3, (b >> 2) & 3, b & 3 to four consecutive samples. Capacity is ⌊6 · w · h / 8⌋ − 12 bytes — one quarter of noise mode.

// write
let bit = 0
for (let i = 0; bit < bytes.length * 8; i++) {
  if (i % 4 === 3) continue
  const b = bytes[bit >> 3]
  const two = (b >> (6 - (bit & 7))) & 3
  data[i] = (data[i] & 0xfc) | two
  bit += 2
}

// read n bytes
let acc = 0, bits = 0
for (let i = 0, j = 0; j < n; i++) {
  if (i % 4 === 3) continue
  acc = (acc << 2) | (data[i] & 3)
  if ((bits += 2) === 8) { out[j++] = acc; acc = 0; bits = 0 }
}
lsb mode — write and read

Modifying the two low bits changes each channel by at most ±3 out of 255 — below the just-noticeable difference on typical displays, which is what makes the cover image visually intact. If no cover is supplied, encoders SHOULD fill the canvas with a solid colour so the output is still a valid, recognisable icon.

Capacity table

Encoders SHOULD choose the smallest square size from the recommended set that fits the container. Non-square or non-listed sizes are permitted by the format but browsers may refuse to display favicons outside common sizes.

SizePixelsnoise capacitylsb capacity
16×16256756 B · 756 B180 B · 180 B
32×321,0243,060 B · 2.99 KB756 B · 756 B
48×482,3046,900 B · 6.74 KB1,716 B · 1.68 KB
64×644,09612,276 B · 12.0 KB3,060 B · 2.99 KB
96×969,21627,636 B · 27.0 KB6,900 B · 6.74 KB
128×12816,38449,140 B · 48.0 KB12,276 B · 12.0 KB
192×19236,864110,580 B · 108.0 KB27,636 B · 27.0 KB
256×25665,536196,596 B · 192.0 KB49,140 B · 48.0 KB

Mode detection

The mode is not stored in the header — it cannot be, since one must know the mode to read the header. Decoders MUST instead try the modes in the fixed order noise, then lsb, reading 12 bytes under each mapping and accepting the first whose magic and version match. Because noise-mode headers occupy full bytes, a genuine noise container will never accidentally parse as lsb first; the reverse is prevented by the ordering.