FWC-1§8Normative
Runtime loader
The host page is a constant. The loader inside it is under 1 KB, has no dependencies, and turns a favicon into a document.
The host page
A host page MUST contain exactly one <link rel="icon"> pointing at the FWC-1 PNG, and a script that implements the decoding procedure of §7 against that link. It SHOULD contain a <noscript> explanation and nothing else that could be mistaken for content. The reference host page is generated by buildHostPage():
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Site title</title>
<link rel="icon" type="image/png" href="/s/username/favicon.png?v=1725800000">
<script>/* loader, see below */</script>
</head>
<body>
<noscript>This page lives inside its favicon. Enable JavaScript to unpack it.</noscript>
</body>
</html>Loader requirements
- MUST load the icon with
crossOrigin = "anonymous"so thatgetImageDatais permitted when the PNG is served from a CDN or another origin. Hosts MUST therefore serve the PNG withAccess-Control-Allow-Origin: *. - MUST size the canvas from
naturalWidth/naturalHeightand MUST NOT scale, smooth, or otherwise resample the image before reading pixels. - MUST implement the mode mapping (§4) that matches the icon. A loader MAY be specialised to a single mode at generation time to save bytes — the reference encoder emits a noise-only or lsb-only loader — but MUST then be paired only with icons of that mode.
- MUST replace the document via
document.open(); document.write(html); document.close()or an equivalent that yields a fresh parsing context, so that the payload's own<html>,<head>and inline scripts behave as if the page had been served directly. - MUST fail silently on
not-a-container(leave the host page as-is) and MUST surfacebad-passwordto the user.
Reference loader (noise mode, unencrypted)
(async () => {
const L = document.querySelector('link[rel~="icon"]'); if (!L) return
const I = new Image; I.crossOrigin = 'anonymous'; I.src = L.href; await I.decode()
const C = document.createElement('canvas'); C.width = I.naturalWidth; C.height = I.naturalHeight
const X = C.getContext('2d', { willReadFrequently: true }); X.drawImage(I, 0, 0)
const D = X.getImageData(0, 0, C.width, C.height).data
const R = n => { const o = new Uint8Array(n); for (let i = 0, j = 0; j < n; i++) { if (i % 4 != 3) o[j++] = D[i] } return o }
const H = R(12); if (H[0] != 70 || H[1] != 87) return
const f = H[3]; const n = (H[4] << 24 | H[5] << 16 | H[6] << 8 | H[7]) >>> 0
let b = R(12 + n).slice(12)
if (f & 2) b = new Uint8Array(await new Response(
new Blob([b]).stream().pipeThrough(new DecompressionStream('deflate-raw'))).arrayBuffer())
const h = new TextDecoder().decode(b)
document.open(); document.write(h); document.close()
})()Minified, this is ~620 bytes; the encrypted variant adds ~400 bytes of PBKDF2/AES-GCM glue. The loader deliberately omits the CRC check to stay small — the host page trusts its own origin to serve an intact icon — whereas general-purpose decoders such as the Unpack panel MUST implement it.
Behaviour of the unpacked document
- The unpacked document inherits the host page's origin and URL. Relative links resolve against the host URL;
locationis unchanged. Authors SHOULD use absolute URLs for external resources. - The
<link rel="icon">is replaced along with everything else. If the payload wants the tab to keep showing the same icon, it MUST include its own<link rel="icon">— pointing at the same PNG is the idiomatic choice. - Search engines and social crawlers see the host page, not the payload. §9 describes the optional server-side rendering path for bots.