SLOPSHOPPER

QRX

Branded, print-ready art QR codes that always scan. Describe a picture and a link; QRX paints the code, verifies it decodes, and hosts it on a qrx.to short…

newrowsprocess
★ 3v1.2.1MITupdated 2026-10-13qrxcodes/qrx-mcp
A shopper browsing a rack in a slop shop
README

QRX

Art QR codes that always scan.

QRX makes branded, print-ready QR codes for posters, menus, packaging and signage. Describe the look you want and give it a link: QRX paints the code into artwork, checks that it decodes before handing it over, and points it at a hosted short link, https://qrx.to/<id> on qrx.codes, that you can re-point later on a paid plan. This repository connects QRX to Claude, ChatGPT, Cursor, VS Code and other MCP clients through the remote server at https://qrx.codes/mcp.

Install in VS Code Install in Cursor Glama Licence: MIT

Listed in the official MCP Registry as codes.qrx/mcp.

What you can do

  • Make a code: "Make a QR code for https://example.com/menu that looks like a watercolour café street."
  • Check on it: generation usually takes under a minute; the agent waits for it and returns the image and short link.
  • Find old codes: list the codes on your account, newest first.
  • Re-point a code: send an existing printed code to a new link, with no reprint (on a paid plan).
  • Check your allowance: see how many codes you have left today.

Connection details

Server URLhttps://qrx.codes/mcp
TransportStreamable HTTP (stateless)
AuthRequired to connect. Sign in with your QRX account (MCP authorization, OAuth 2.1); clients that support it need only the URL. The authorization server is https://clerk.qrx.codes (dynamic client registration or client ID metadata documents, PKCE S256, refresh tokens); scopes qrx:read qrx:generate
Auth with an API keyAuthorization: Bearer qrx_… (preferred), or X-API-Key: qrx_… for clients that can't add the Bearer prefix. If both are sent, Authorization wins. For clients without sign-in, scripts and CI
Without authEvery request, including initialize, answers 401 with a WWW-Authenticate header pointing at https://qrx.codes/.well-known/oauth-protected-resource/mcp, so the client asks you to sign in as soon as it connects
Public tool listhttps://qrx.codes/.well-known/mcp/server-card.json
Docsqrx.codes/developers/mcp (public: server URL, client set-up, tools, limits, privacy)

Sign in or use an API key

Every client below that takes a URL signs in with your QRX account: add the URL, then sign in (free) when the client asks as it connects. There is nothing to copy or keep secret.

For clients that only run local servers, and for scripts and CI, use an API key. Any client below can use one instead of signing in:

  1. Sign in at qrx.codes (free).
  2. Open qrx.codes/developers/keys and create a key. It starts with qrx_.
  3. Keep it secret. The examples below read it from an environment variable called QRX_API_KEY wherever the client allows that:
   # macOS / Linux
   export QRX_API_KEY="qrx_live_…"
   # Windows PowerShell (current user, persistent)
   [Environment]::SetEnvironmentVariable("QRX_API_KEY", "qrx_live_…", "User")

Revoke a key at any time from the same page.

Install

Option A: plugin (MCP server, the QRX skill and the code card)

/plugin marketplace add qrxcodes/qrx-mcp
/plugin install qrx@qrx

Then run /mcp, select qrx and sign in to QRX. The plugin also adds the qrx skill, which teaches Claude how to write good art prompts, wait for the code and prepare it for print.

The plugin also shows each finished code in the transcript, under the QRX tool call: its picture, the short link it scans to and a link to its QRX page, with a line while the code is still being painted. It is a mod, so it draws in the terminal (Claude Code 2.1.287 or later) and the Desktop app's Code tab; it fetches the picture with curl. Terminals with kitty graphics (kitty, Ghostty) show the full picture; other terminals show it in coloured character cells, with a link to the full-quality image.

Option B: MCP server only

claude mcp add --transport http qrx https://qrx.codes/mcp --scope user

Then run /mcp, select qrx and sign in. Or commit this to a project's .mcp.json so the whole team gets it (each person signs in to their own account):

{
  "mcpServers": {
    "qrx": {
      "type": "http",
      "url": "https://qrx.codes/mcp"
    }
  }
}

With an API key instead (for example with claude -p or in CI):

claude mcp add --transport http qrx https://qrx.codes/mcp \
  --header "Authorization: Bearer $QRX_API_KEY" --scope user

A configured Authorization header replaces sign-in; remove it to sign in instead.

Claude Desktop uses the same connector settings as claude.ai; you don't edit claude_desktop_config.json for a remote server.

  1. Open Customize → Connectors, select + Add, then Add custom connector. On Team and Enterprise, an Owner adds it under Organization settings → Connectors first, and each member then selects Connect.
  2. Name: QRX. URL: https://qrx.codes/mcp.
  3. Authentication: Sign in now, then sign in to QRX when Claude asks.
  4. Save, then turn QRX on in a chat from the connectors menu.

With an API key instead, choose No sign-in and add Authorization with the value Bearer qrx_live_… under Request headers (where your organisation has request headers). Authentication can't be changed after saving; remove the connector and add it again.

Free Claude plans can add one custom connector.

  1. Open chatgpt.com/plugins on the web, select +, then Add custom MCP server.
  2. Name: QRX. Server URL: https://qrx.codes/mcp.
  3. Authentication: OAuth. Accept the risk warning and select Create as a plugin.
  4. Sign in to QRX when ChatGPT asks.

ChatGPT's custom MCP servers don't take API keys. Workspace admins can restrict custom MCP servers on Business and Enterprise plans.

Use the Install in Cursor button above, or add this to ~/.cursor/mcp.json (all projects) or .cursor/mcp.json (one project):

{
  "mcpServers": {
    "qrx": {
      "url": "https://qrx.codes/mcp"
    }
  }
}

Cursor opens a browser to sign in to QRX when it connects.

With an API key instead:

{
  "mcpServers": {
    "qrx": {
      "url": "https://qrx.codes/mcp",
      "headers": { "Authorization": "Bearer ${env:QRX_API_KEY}" }
    }
  }
}

Use the Install in VS Code button above, or add this to .vscode/mcp.json:

{
  "servers": {
    "qrx": {
      "type": "http",
      "url": "https://qrx.codes/mcp"
    }
  }
}

From a terminal:

code --add-mcp '{"name":"qrx","type":"http","url":"https://qrx.codes/mcp"}'

VS Code opens a browser to sign in to QRX when it connects.

With an API key instead (VS Code asks for it and stores it securely):

{
  "inputs": [
    {
      "type": "promptString",
      "id": "qrx-api-key",
      "description": "QRX API key (starts with qrx_)",
      "password": true
    }
  ],
  "servers": {
    "qrx": {
      "type": "http",
      "url": "https://qrx.codes/mcp",
      "headers": { "Authorization": "Bearer ${input:qrx-api-key}" }
    }
  }
}

Edit mcp_config.json (macOS/Linux ~/.config/devin/mcp_config.json, Windows %APPDATA%\devin\mcp_config.json; older Windsurf installs use ~/.codeium/windsurf/mcp_config.json):

{
  "mcpServers": {
    "qrx": {
      "serverUrl": "https://qrx.codes/mcp"
    }
  }
}

Note the key is serverUrl, not url. Windsurf asks you to sign in to QRX when it connects. With an API key instead, add "headers": { "Authorization": "Bearer ${env:QRX_API_KEY}" }.

Open MCP Servers → Configure MCP Servers and add to cline_mcp_settings.json:

{
  "mcpServers": {
    "qrx": {
      "type": "streamableHttp",
      "url": "https://qrx.codes/mcp",
      "disabled": false,
      "autoApprove": ["list_styles", "get_qr_code", "list_qr_codes", "get_account", "get_profile"]
    }
  }
}

Cline asks you to sign in to QRX when it connects. In the Cline CLI, run cline mcp, choose qrx and Authorize OAuth. With an API key instead, add "headers": { "Authorization": "Bearer qrx_live_…" }.

Add to settings.json:

{
  "context_servers": {
    "qrx": {
      "url": "https://qrx.codes/mcp"
    }
  }
}

Zed asks you to sign in to QRX when it connects. With an API key instead, add "headers": { "Authorization": "Bearer qrx_live_…" }; Zed then skips sign-in.

Run goose configure → Add Extension → Remote Extension (Streamable HTTP) and enter https://qrx.codes/mcp. Or edit config.yaml (macOS/Linux ~/.config/goose/config.yaml, Windows %APPDATA%\Block\goose\config\config.yaml):

extensions:
  qrx:
    name: qrx
    type: streamable_http
    enabled: true
    uri: https://qrx.codes/mcp
    timeout: 300

Goose opens your browser to sign in to QRX when it connects. With an API key instead, add the header Authorization: Bearer qrx_live_… (headers: in config.yaml).

codex mcp add qrx --url https://qrx.codes/mcp

Codex finds QRX's sign-in and opens your browser. Run codex mcp login qrx to sign in again later. The entry in ~/.codex/config.toml is just:

[mcp_servers.qrx]
url = "https://qrx.codes/mcp"

With an API key instead, add bearer_token_env_var = "QRX_API_KEY" to that entry.

gemini mcp add --transport http -s user qrx https://qrx.codes/mcp

Gemini CLI opens your browser to sign in to QRX when it connects; /mcp auth qrx signs in again later. Or in ~/.gemini/settings.json (note httpUrl, which selects Streamable HTTP):

{
  "mcpServers": {
    "qrx": {
      "httpUrl": "https://qrx.codes/mcp"
    }
  }
}

With an API key instead:

gemini mcp add --transport http -s user --header "Authorization: Bearer $QRX_API_KEY" qrx https://qrx.codes/mcp

In the Program tab, choose Install → Edit mcp.json and add:

{
  "mcpServers": {
    "qrx": {
      "url": "https://qrx.codes/mcp"
    }
  }
}

LM Studio asks you to sign in to QRX when it connects. With an API key instead, add "headers": { "Authorization": "Bearer qrx_live_…" }.

The skills/qrx folder follows the Agent Skills standard, so the same skill works in Codex, Gemini CLI, GitHub Copilot, Cursor, Goose and others. Install it with the cross-agent installer:

npx skills add qrxcodes/qrx-mcp

The skill expects the QRX MCP server to be connected (see your client above), or a QRX_API_KEY for the REST API.

gemini extensions install https://github.com/qrxcodes/qrx-mcp

Gemini CLI opens your browser to sign in to QRX when it connects; /mcp auth qrx signs in again later.

Clients that support MCP authorization need only https://qrx.codes/mcp: the server answers 401 until you sign in, so they ask when they connect. Otherwise point a Streamable HTTP client at the URL and send Authorization: Bearer qrx_… (or X-API-Key: qrx_…). Clients that only run local stdio servers can use @qrxcodes/mcp, which forwards to the same server with your API key:

{
  "mcpServers": {
    "qrx": {
      "command": "npx",
      "args": ["-y", "@qrxcodes/mcp"],
      "env": { "QRX_API_KEY": "qrx_live_…" }
    }
  }
}

Tools

Connecting needs sign-in or an API key. The tool list is public at https://qrx.codes/.well-known/mcp/server-card.json.

ToolWhat it doesRead-only
generate_qr_codeStarts a new art QR code from a prompt and a link. Returns the code's id straight away.No
vary_qr_codeMakes a new code from one of your codes with a change to its picture, such as "more trees". The new code has its own short link to the same destination; the original doesn't change.No
get_qr_codeGets a code's status, image and short link. Waits up to wait seconds (0 to 25, default 25) for a code that's still being painted.Yes
list_qr_codesLists your codes, newest first, a page at a time (limit 1 to 100, default 20; pass nextCursor as cursor).Yes
change_qr_code_destinationPoints an existing link code at a new absolute http or https URL (on a paid plan). The printed code keeps working. Marked destructive, because it replaces the old destination.No
get_accountShows your plan, how many codes are left today, when the count resets and what the account can do.Yes
get_profileShows which QRX account is connected.Yes
list_stylesLists the looks you can pass to generate_qr_code.Yes

In clients that show interactive views (MCP Apps), such as Claude, the code also appears in a card that fills in when it is ready.

Example prompts

  1. "Make a QR code for https://example.com/menu that looks like a watercolour street of cafés."
  2. "Design a QR code for our wedding RSVP page, https://example.com/rsvp, as eucalyptus leaves and gold foil on cream paper."
  3. "I need a QR code for a gig poster that opens https://example.com/tickets. Neon synthwave city at night."
  4. "Show me the styles QRX has, then make a code for https://example.com in the one that best suits a bakery."
  5. "List my QR codes from this week and give me the short links."
  6. "The flyer code we printed last month should now open https://example.com/spring-sale instead."
  7. "How many QR codes can I still make today?"
  8. "Make three variations of a QR code for https://example.com/shop with a Japanese woodblock wave, and tell me which scans are verified."

How generation works

  • generate_qr_code returns within a few seconds with an id and status: processing. The artwork usually takes under a minute on a GPU.
  • get_qr_code then waits for up to 25 seconds per call (wait, 0–25). Agents call it again until the status is succeeded (or failed). In MCP Apps clients the card fills in with the image when it's ready.
  • A finished code includes the image URL, the short link it encodes (shortUrl: https://qrx.to/<id> on qrx.codes) and the destination it redirects to. Every code is decoded before it's marked succeeded. A code's id and short link never change.
  • Each code you start counts against your plan, even if you don't keep the result.

Plans and limits

A free account has a daily allowance of codes; a paid plan has more, and lets you re-point links. Current plans are at https://qrx.codes/upgrade.

The API, the MCP server and the website share one allowance. Each key is also rate limited (30 requests a minute and 6 new codes per 10 seconds). If you hit a limit the tool returns an error; get_account shows what is left and when the daily count resets. Limits: qrx.codes/developers.

What this repository contains

PathFor
.claude-plugin/plugin.json, .claude-plugin/marketplace.json, .mcp.jsonClaude Code plugin and its marketplace (/plugin marketplace add qrxcodes/qrx-mcp)
skills/qrx/The qrx skill (Agent Skills standard): prompting, waiting for the code, print checks
gemini-extension.json, GEMINI.mdGemini CLI extension
server.jsonThe entry for the official MCP Registry
glama.jsonGlama maintainer claim
llms-install.mdStep-by-step set-up written for AI agents (Cline and others)

None of these files run code on your machine. They only point your client at https://qrx.codes/mcp.

Data and network use

Connecting to https://qrx.codes/mcp needs sign-in or an API key; without one the server answers 401. Clients send requests only to https://qrx.codes/mcp (and, when the skill falls back to the REST API, to https://qrx.codes/v1). Each request carries your sign-in token or API key and the prompt, destination link and style you asked for. Signing in goes through QRX's sign-in service at https://clerk.qrx.codes. A finished code's imageUrl is on https://qrx.codes/media/, which redirects to QRX's image CDN.

Privacy, terms and support

QRX stores the prompts, links and images you create so they appear in your account. It doesn't read your chat history; it only sees the arguments the client sends to its tools.

Licence

The configuration files, skill and documentation in this repository are released under the MIT licence. The QRX service itself is a hosted product, governed by the QRX terms.

<!-- Docs sources (re-check these when clients change; all read 9 Oct 2026):

-->

Source 5 files
hooks/qrx-card.tsx 184 lines
1// The QRX code card: shows a finished code's picture under the QRX tool call in Claude Code's transcript, with its
2// short link and page, and a line while the code is still being painted. The tool's own text is unchanged for Claude
3// and for every client that draws nothing.
4import type { EngineInterface, Register, RenderElement, RenderInput } from 'claude-code'
5
6import { codeOf, isCodeTool, isQrxUrl, type Code } from './lib/code.ts'
7import { fullQualityUrl, pictureKind } from './lib/picture.ts'
8import { decodePng, encodePng, fromBase64, shrink, toBase64, type Rgb } from './lib/png.ts'
9
10type Picture =
11  | { state: 'loading' }
12  | { state: 'failed'; reason: string }
13  | { state: 'ready'; png: string; thumb: string; preview: Rgb }
14
15/** Pictures by code id: a code's image never changes once it is made. */
16const pictures = new Map<string, Picture>()
17
18/** The thumbnail the desktop and editor draw: small enough that the SVG holding it stays under 131,072 characters. */
19const THUMB = 160
20/** The pixels the terminal's cell grid is drawn from. */
21const PREVIEW = 96
22/** The widest picture the terminal draws, in columns; two pixels go in each cell, one above the other. */
23const MAX_COLUMNS = 48
24const UPPER_HALF = 0x2580
25
26const slash = (path: string) => path.split(String.fromCharCode(92)).join('/')
27
28let tempDir = '/tmp'
29let terminalImages = false
30
31export const register: Register = on => {
32  on('session.start', async ($, e, next) => {
33    const temp = slash((await $.env.get('TEMP')) || (await $.env.get('TMPDIR')) || '/tmp')
34    tempDir = temp.endsWith('/') ? temp.slice(0, -1) : temp
35    const term = `${(await $.env.get('TERM')) ?? ''} ${(await $.env.get('TERM_PROGRAM')) ?? ''}`.toLowerCase()
36    const multiplexed = Boolean((await $.env.get('TMUX')) || (await $.env.get('STY')))
37    // Claude Code draws an Image as pixels only where the terminal has kitty's placeholder images (kitty, Ghostty)
38    terminalImages =
39      (await $.env.get('CLAUDE_CODE_FORCE_TERMINAL_IMAGES')) === '1' ||
40      (!multiplexed && (term.includes('kitty') || term.includes('ghostty') || Boolean(await $.env.get('KITTY_WINDOW_ID'))))
41    return next(e)
42  })
43
44  // a call drawn on its own row: the card takes the place of the result's text
45  on('ui.render', { component: 'ToolResult' }, async ($, e, next) => {
46    if (!isCodeTool(e.props.tool) || e.props.isErrored) return next(e)
47    const code = codeOf(e.props.output)
48    if (!code || code.status === 'failed') return next(e)
49    return card($, e, code)
50  })
51
52  // calls folded into a group's count line (the terminal folds MCP calls): the cards go under that line, one per code,
53  // from its latest call, so a code that was started and then fetched shows once
54  on('ui.render', { component: 'ToolGroup' }, async ($, e, next) => {
55    const latest = new Map<string, Code>()
56    for (const call of e.props.calls) {
57      if (!isCodeTool(call.tool) || call.isRunning || call.isErrored || call.isInterrupted) continue
58      const code = codeOf(call.output)
59      if (!code) continue
60      latest.delete(code.id)
61      latest.set(code.id, code)
62    }
63    const codes = [...latest.values()].filter(code => code.status !== 'failed')
64    if (!codes.length || e.props.isExpanded) return next(e)
65    const { Box } = $.ui.resolve(e)
66    const line = await next(e)
67    const cards: RenderElement[] = []
68    for (const code of codes) cards.push(await card($, e, code))
69    return (
70      <Box flexDirection="column">
71        {line}
72        {cards}
73      </Box>
74    )
75  })
76}
77
78/** The card for one code: its picture as the surface can draw it, the link it scans to and its QRX page. */
79async function card($: EngineInterface, e: RenderInput, code: Code): Promise<RenderElement> {
80  const { Box, Text, Link } = $.ui.resolve(e)
81
82  if (code.status === 'processing') {
83    return (
84      <Box key={`qrx-${code.id}`} flexDirection="column" paddingLeft={2}>
85        <Text dimColor>QRX is painting code {code.id}. It usually takes under a minute.</Text>
86      </Box>
87    )
88  }
89
90  const imageUrl = isQrxUrl(code.imageUrl) ? code.imageUrl : undefined
91  if (imageUrl && !pictures.has(code.id)) void load($, { ...code, imageUrl })
92  const picture = pictures.get(code.id)
93
94  let view: RenderElement | null = null
95  const alt = `QRX code ${code.id}${code.shortUrl ? `, scans to ${code.shortUrl}` : ''}`
96  if (!picture || picture.state === 'loading') view = <Text dimColor>Loading the code’s picture…</Text>
97  else if (picture.state === 'failed') view = imageUrl ? <Link href={imageUrl} label="Open the code’s picture (PNG)" /> : null
98  else if (e.surface === 'terminal') {
99    const { Image, Raster } = $.ui.resolve(e)
100    const width = e.viewport?.columns ?? 80
101    const columns = Math.max(16, Math.min(MAX_COLUMNS, width - 6)) & ~1
102    const rows = Math.round((columns * picture.preview.height) / picture.preview.width / 2)
103    const kind = pictureKind(e.surface, terminalImages, picture.png.length)
104    const fullQuality = fullQualityUrl(kind, code)
105    view =
106      kind === 'pixels' ? (
107        <Image key={`qrx-image-${code.id}`} source={{ png: picture.png }} columns={columns} rows={rows} alt={alt} />
108      ) : (
109        <Box flexDirection="column">
110          <Raster key={`qrx-image-${code.id}`} columns={columns} rows={rows} cells={halfBlocks(picture.preview, columns, rows)} />
111          {fullQuality ? (
112            <Box key={`qrx-full-quality-${code.id}`} flexDirection="row">
113              <Text dimColor>Open </Text>
114              <Link href={fullQuality} />
115              <Text dimColor> for the full-quality image</Text>
116            </Box>
117          ) : null}
118        </Box>
119      )
120  } else {
121    const { Svg } = $.ui.resolve(e)
122    const w = THUMB
123    const h = Math.round((THUMB * picture.preview.height) / picture.preview.width)
124    const source =
125      `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">` +
126      `<image width="${w}" height="${h}" href="data:image/png;base64,${picture.thumb}"/></svg>`
127    view = <Svg alt={alt} width={240} height={Math.round((240 * h) / w)} source={source} />
128  }
129
130  return (
131    <Box key={`qrx-${code.id}`} flexDirection="column" paddingLeft={2}>
132      <Text bold>QR code {code.id} is ready</Text>
133      {view}
134      {code.shortUrl ? (
135        <Text>
136          Scans open {code.destination ?? 'its link'} through {code.shortUrl}
137        </Text>
138      ) : null}
139      {code.pageUrl ? <Link href={code.pageUrl} label="View or download it in QRX" /> : null}
140    </Box>
141  )
142}
143
144/** Fetches a finished code's PNG and keeps it, decoded and shrunk, then redraws. */
145async function load($: EngineInterface, code: Code & { imageUrl: string }) {
146  pictures.set(code.id, { state: 'loading' })
147  try {
148    // $.http.fetch hands back text, so the PNG comes down to a file and is read back as bytes
149    const file = `${tempDir}/qrx-card-${code.id}.png`
150    const run = await $.process.run(['curl', '-sSfL', '--max-time', '30', '-o', file, code.imageUrl], { timeoutMs: 40_000 })
151    if (run.exitCode !== 0) throw new Error(run.stderr.trim() || `curl exited ${run.exitCode}`)
152    const { base64 } = (await $.fs.read(file, { as: 'bytes' })) as { base64: string }
153    const image = decodePng(fromBase64(base64))
154    pictures.set(code.id, {
155      state: 'ready',
156      png: base64,
157      thumb: toBase64(encodePng(shrink(image, THUMB, Math.round((THUMB * image.height) / image.width)))),
158      preview: shrink(image, PREVIEW, Math.round((PREVIEW * image.height) / image.width)),
159    })
160  } catch (error) {
161    pictures.set(code.id, { state: 'failed', reason: error instanceof Error ? error.message : String(error) })
162  }
163  $.ui.invalidate('ui.render')
164}
165
166/** Packs a picture into a Raster's cells: an upper half block per cell, its top pixel the ink and its bottom the paper. */
167function halfBlocks(preview: Rgb, columns: number, rows: number) {
168  const pixels = shrink(preview, columns, rows * 2)
169  const words = new Uint32Array(columns * rows * 3)
170  const color = (x: number, y: number) => {
171    const i = (y * columns + x) * 3
172    return (pixels.data[i] << 16) | (pixels.data[i + 1] << 8) | pixels.data[i + 2]
173  }
174  for (let y = 0; y < rows; y++) {
175    for (let x = 0; x < columns; x++) {
176      const w = (y * columns + x) * 3
177      words[w] = UPPER_HALF
178      words[w + 1] = color(x, y * 2)
179      words[w + 2] = color(x, y * 2 + 1)
180    }
181  }
182  return toBase64(new Uint8Array(words.buffer))
183}
184
hooks/lib/code.ts 113 lines
1// Reads a QRX code out of a QRX MCP tool's result, as Claude Code stores it for the transcript.
2
3export type CodeStatus = 'processing' | 'succeeded' | 'failed'
4
5export type Code = {
6  id: string
7  status: CodeStatus
8  imageUrl?: string
9  shortUrl?: string
10  pageUrl?: string
11  destination?: string
12}
13
14/** The QRX tools whose result is one code, under any server name that holds `qrx` (plugin or `claude mcp add`). */
15const CODE_TOOL = /^mcp__[\w-]*qrx[\w-]*__(generate_qr_code|vary_qr_code|get_qr_code)$/
16
17export const isCodeTool = (tool: string) => CODE_TOOL.test(tool)
18
19/** Only images QRX serves are fetched: qrx.codes and its subdomains (dev), and the qrx.to short links. */
20export function isQrxUrl(value: unknown): value is string {
21  if (typeof value !== 'string') return false
22  try {
23    const url = new URL(value)
24    const host = url.hostname
25    return url.protocol === 'https:' && (host === 'qrx.codes' || host.endsWith('.qrx.codes') || host === 'qrx.to')
26  } catch {
27    return false
28  }
29}
30
31const ID = /^[a-z0-9]{1,64}$/i
32const STATUSES: readonly string[] = ['processing', 'succeeded', 'failed']
33
34function fromObject(value: Record<string, unknown>): Code | undefined {
35  if (typeof value.id !== 'string' || !ID.test(value.id) || typeof value.status !== 'string') return undefined
36  if (!STATUSES.includes(value.status)) return undefined
37  return {
38    id: value.id,
39    status: value.status as CodeStatus,
40    imageUrl: isQrxUrl(value.imageUrl) ? value.imageUrl : undefined,
41    shortUrl: isQrxUrl(value.shortUrl) ? value.shortUrl : undefined,
42    pageUrl: isQrxUrl(value.pageUrl) ? value.pageUrl : undefined,
43    destination: typeof value.destination === 'string' ? value.destination : undefined,
44  }
45}
46
47const READY = /QR code ([a-z0-9]+) is ready\./i
48const PAINTING = /QR code ([a-z0-9]+) is still being painted/i
49const FAILED = /QR code ([a-z0-9]+) could not be made/i
50const IMAGE = /Image \(PNG\): (\S+)/
51const SCANS = /Scans open (\S+) through (\S+?)\.?$/m
52const PAGE = /View or download it in QRX: (\S+)/
53
54/** The text the server writes for a code (see codeLine in the QRX API): used when no structured result is kept. */
55function fromText(text: string): Code | undefined {
56  const ready = READY.exec(text)
57  if (ready) {
58    const image = IMAGE.exec(text)?.[1]
59    const scans = SCANS.exec(text)
60    const page = PAGE.exec(text)?.[1]
61    return {
62      id: ready[1],
63      status: 'succeeded',
64      imageUrl: isQrxUrl(image) ? image : undefined,
65      shortUrl: isQrxUrl(scans?.[2]) ? scans?.[2] : undefined,
66      destination: scans?.[1],
67      pageUrl: isQrxUrl(page) ? page : undefined,
68    }
69  }
70  const painting = PAINTING.exec(text)
71  if (painting) return { id: painting[1], status: 'processing' }
72  const failed = FAILED.exec(text)
73  if (failed) return { id: failed[1], status: 'failed' }
74  return undefined
75}
76
77/**
78 * Finds the code in a tool result of any shape Claude Code keeps: the structured result, a list of content blocks,
79 * an object holding either, or the text alone. The structured result wins over the text.
80 */
81export function codeOf(output: unknown): Code | undefined {
82  const texts: string[] = []
83  let found: Code | undefined
84  const visit = (value: unknown, depth: number) => {
85    if (found || depth > 6 || value === null || value === undefined) return
86    if (typeof value === 'string') {
87      const trimmed = value.trim()
88      if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
89        try {
90          visit(JSON.parse(trimmed), depth + 1)
91          return
92        } catch {
93          // not JSON: read it as text
94        }
95      }
96      texts.push(value)
97      return
98    }
99    if (Array.isArray(value)) {
100      for (const item of value) visit(item, depth + 1)
101      return
102    }
103    if (typeof value === 'object') {
104      const record = value as Record<string, unknown>
105      found = fromObject(record)
106      if (found) return
107      for (const key of Object.keys(record)) visit(record[key], depth + 1)
108    }
109  }
110  visit(output, 0)
111  return found ?? fromText(texts.join('\n'))
112}
113
hooks/lib/picture.ts 19 lines
1// How the card draws a code's picture on each surface, and when it points to the full-quality image.
2import type { Code } from './code.ts'
3
4/** `pixels`: an Image (kitty, Ghostty); `cells`: a Raster of half blocks (other terminals); `svg`: the Desktop app and the editor. */
5export type PictureKind = 'pixels' | 'cells' | 'svg'
6
7/** The largest PNG an Image takes (2 MiB decoded), as base64. */
8export const MAX_IMAGE_BASE64 = Math.floor(((2 * 1024 * 1024) / 3) * 4)
9
10export function pictureKind(surface: string, terminalImages: boolean, pngBase64Length: number): PictureKind {
11  if (surface !== 'terminal') return 'svg'
12  return terminalImages && pngBase64Length <= MAX_IMAGE_BASE64 ? 'pixels' : 'cells'
13}
14
15/** The link under a cell-drawn picture, which loses detail: the code's PNG, or its QRX page when there is no PNG link. */
16export function fullQualityUrl(kind: PictureKind, code: Code): string | undefined {
17  return kind === 'cells' ? (code.imageUrl ?? code.pageUrl) : undefined
18}
19
hooks/lib/png.ts 218 lines
1// Just enough PNG for the code card: read a code's PNG into pixels, shrink it, and write a small PNG back.
2// Mods run with no DOM, no Node and no DecompressionStream, so the inflate is vendored and the writer uses stored
3// (uncompressed) deflate blocks, which every PNG reader accepts.
4import inflate from './inflate.js'
5
6// the environment has the ES2026 base64 methods on Uint8Array; TypeScript's lib does not declare them yet
7type Base64Bytes = { toBase64(this: Uint8Array): string }
8export const toBase64 = (bytes: Uint8Array): string => (bytes as Uint8Array & Base64Bytes).toBase64()
9export const fromBase64 = (text: string): Uint8Array =>
10  (Uint8Array as unknown as { fromBase64(text: string): Uint8Array }).fromBase64(text)
11
12/** Pixels as RGB, 3 bytes each, row-major. */
13export type Rgb = { width: number; height: number; data: Uint8Array }
14
15const SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10]
16const CHANNELS: Record<number, number> = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 }
17
18const u32 = (b: Uint8Array, i: number) => ((b[i] << 24) | (b[i + 1] << 16) | (b[i + 2] << 8) | b[i + 3]) >>> 0
19const typeOf = (b: Uint8Array, i: number) => String.fromCharCode(b[i], b[i + 1], b[i + 2], b[i + 3])
20
21/** Decodes an 8-bit, non-interlaced PNG (the kind QRX serves) into RGB, flattening alpha onto white. */
22export function decodePng(bytes: Uint8Array): Rgb {
23  for (let i = 0; i < 8; i++) if (bytes[i] !== SIGNATURE[i]) throw new Error('not a PNG')
24  let width = 0
25  let height = 0
26  let colorType = 0
27  let palette: Uint8Array | undefined
28  let alphas: Uint8Array | undefined
29  const parts: Uint8Array[] = []
30  let total = 0
31  for (let at = 8; at + 8 <= bytes.length; ) {
32    const length = u32(bytes, at)
33    const type = typeOf(bytes, at + 4)
34    const body = bytes.subarray(at + 8, at + 8 + length)
35    if (type === 'IHDR') {
36      width = u32(body, 0)
37      height = u32(body, 4)
38      colorType = body[9]
39      if (body[8] !== 8) throw new Error(`unsupported PNG bit depth ${body[8]}`)
40      if (body[12] !== 0) throw new Error('interlaced PNGs are not supported')
41      if (!(colorType in CHANNELS)) throw new Error(`unsupported PNG color type ${colorType}`)
42    } else if (type === 'PLTE') palette = body
43    else if (type === 'tRNS') alphas = body
44    else if (type === 'IDAT') {
45      parts.push(body)
46      total += body.length
47    } else if (type === 'IEND') break
48    at += 12 + length
49  }
50  if (!width || !height || !parts.length) throw new Error('PNG has no image data')
51
52  const zlib = new Uint8Array(total)
53  let offset = 0
54  for (const part of parts) {
55    zlib.set(part, offset)
56    offset += part.length
57  }
58  const bpp = CHANNELS[colorType]
59  const stride = width * bpp
60  // the zlib header is 2 bytes; the Adler-32 at the end is not checked
61  const raw = inflate(zlib.subarray(2), new Uint8Array(height * (stride + 1))) as Uint8Array
62  if (raw.length !== height * (stride + 1)) throw new Error('PNG image data is truncated')
63
64  const pixels = new Uint8Array(height * stride)
65  for (let y = 0; y < height; y++) {
66    const filter = raw[y * (stride + 1)]
67    const src = y * (stride + 1) + 1
68    const row = y * stride
69    const up = row - stride
70    for (let x = 0; x < stride; x++) {
71      const a = x >= bpp ? pixels[row + x - bpp] : 0
72      const b = y > 0 ? pixels[up + x] : 0
73      const c = x >= bpp && y > 0 ? pixels[up + x - bpp] : 0
74      let value = raw[src + x]
75      if (filter === 1) value += a
76      else if (filter === 2) value += b
77      else if (filter === 3) value += (a + b) >> 1
78      else if (filter === 4) {
79        const p = a + b - c
80        const pa = Math.abs(p - a)
81        const pb = Math.abs(p - b)
82        const pc = Math.abs(p - c)
83        value += pa <= pb && pa <= pc ? a : pb <= pc ? b : c
84      }
85      pixels[row + x] = value & 255
86    }
87  }
88
89  const data = new Uint8Array(width * height * 3)
90  for (let i = 0, o = 0; i < width * height; i++, o += 3) {
91    let r: number
92    let g: number
93    let b: number
94    let alpha = 255
95    if (colorType === 2 || colorType === 6) {
96      r = pixels[i * bpp]
97      g = pixels[i * bpp + 1]
98      b = pixels[i * bpp + 2]
99      if (colorType === 6) alpha = pixels[i * bpp + 3]
100    } else if (colorType === 3) {
101      const index = pixels[i]
102      r = palette ? palette[index * 3] : 0
103      g = palette ? palette[index * 3 + 1] : 0
104      b = palette ? palette[index * 3 + 2] : 0
105      if (alphas && index < alphas.length) alpha = alphas[index]
106    } else {
107      r = g = b = pixels[i * bpp]
108      if (colorType === 4) alpha = pixels[i * bpp + 1]
109    }
110    data[o] = (r * alpha + 255 * (255 - alpha)) / 255
111    data[o + 1] = (g * alpha + 255 * (255 - alpha)) / 255
112    data[o + 2] = (b * alpha + 255 * (255 - alpha)) / 255
113  }
114  return { width, height, data }
115}
116
117/** Shrinks by averaging each target pixel's box of source pixels. */
118export function shrink(image: Rgb, width: number, height: number): Rgb {
119  const data = new Uint8Array(width * height * 3)
120  for (let y = 0; y < height; y++) {
121    const y0 = Math.floor((y * image.height) / height)
122    const y1 = Math.max(y0 + 1, Math.floor(((y + 1) * image.height) / height))
123    for (let x = 0; x < width; x++) {
124      const x0 = Math.floor((x * image.width) / width)
125      const x1 = Math.max(x0 + 1, Math.floor(((x + 1) * image.width) / width))
126      let r = 0
127      let g = 0
128      let b = 0
129      for (let sy = y0; sy < y1; sy++) {
130        for (let sx = x0; sx < x1; sx++) {
131          const i = (sy * image.width + sx) * 3
132          r += image.data[i]
133          g += image.data[i + 1]
134          b += image.data[i + 2]
135        }
136      }
137      const n = (y1 - y0) * (x1 - x0)
138      const o = (y * width + x) * 3
139      data[o] = r / n
140      data[o + 1] = g / n
141      data[o + 2] = b / n
142    }
143  }
144  return { width, height, data }
145}
146
147const CRC_TABLE = (() => {
148  const table = new Uint32Array(256)
149  for (let n = 0; n < 256; n++) {
150    let c = n
151    for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
152    table[n] = c >>> 0
153  }
154  return table
155})()
156
157function crc32(bytes: Uint8Array) {
158  let c = 0xffffffff
159  for (let i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 255] ^ (c >>> 8)
160  return (c ^ 0xffffffff) >>> 0
161}
162
163function chunk(type: string, body: Uint8Array) {
164  const out = new Uint8Array(12 + body.length)
165  const view = new DataView(out.buffer)
166  view.setUint32(0, body.length)
167  for (let i = 0; i < 4; i++) out[4 + i] = type.charCodeAt(i)
168  out.set(body, 8)
169  view.setUint32(8 + body.length, crc32(out.subarray(4, 8 + body.length)))
170  return out
171}
172
173/** Writes RGB pixels as a PNG whose image data is stored, not compressed. */
174export function encodePng(image: Rgb): Uint8Array {
175  const stride = image.width * 3
176  const raw = new Uint8Array(image.height * (stride + 1))
177  for (let y = 0; y < image.height; y++) raw.set(image.data.subarray(y * stride, (y + 1) * stride), y * (stride + 1) + 1)
178
179  const blocks = Math.max(1, Math.ceil(raw.length / 65535))
180  const zlib = new Uint8Array(2 + raw.length + blocks * 5 + 4)
181  zlib[0] = 0x78
182  zlib[1] = 0x01
183  let at = 2
184  for (let i = 0; i < blocks; i++) {
185    const start = i * 65535
186    const length = Math.min(65535, raw.length - start)
187    zlib[at] = i === blocks - 1 ? 1 : 0
188    zlib[at + 1] = length & 255
189    zlib[at + 2] = length >> 8
190    zlib[at + 3] = ~length & 255
191    zlib[at + 4] = (~length >> 8) & 255
192    zlib.set(raw.subarray(start, start + length), at + 5)
193    at += 5 + length
194  }
195  let s1 = 1
196  let s2 = 0
197  for (let i = 0; i < raw.length; i++) {
198    s1 = (s1 + raw[i]) % 65521
199    s2 = (s2 + s1) % 65521
200  }
201  new DataView(zlib.buffer).setUint32(at, ((s2 << 16) | s1) >>> 0)
202
203  const header = new Uint8Array(13)
204  const view = new DataView(header.buffer)
205  view.setUint32(0, image.width)
206  view.setUint32(4, image.height)
207  header[8] = 8
208  header[9] = 2
209  const parts = [new Uint8Array(SIGNATURE), chunk('IHDR', header), chunk('IDAT', zlib), chunk('IEND', new Uint8Array(0))]
210  const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0))
211  let offset = 0
212  for (const part of parts) {
213    out.set(part, offset)
214    offset += part.length
215  }
216  return out
217}
218
hooks/lib/inflate.js 379 lines
1// tiny-inflate 1.0.3 (https://github.com/foliojs/tiny-inflate), an ES module copy.
2// MIT License, Copyright (c) 2015-present Devon Govett: see LICENSE-tiny-inflate beside this file.
3
4var TINF_OK = 0;
5var TINF_DATA_ERROR = -3;
6
7function Tree() {
8  this.table = new Uint16Array(16);   /* table of code length counts */
9  this.trans = new Uint16Array(288);  /* code -> symbol translation table */
10}
11
12function Data(source, dest) {
13  this.source = source;
14  this.sourceIndex = 0;
15  this.tag = 0;
16  this.bitcount = 0;
17  
18  this.dest = dest;
19  this.destLen = 0;
20  
21  this.ltree = new Tree();  /* dynamic length/symbol tree */
22  this.dtree = new Tree();  /* dynamic distance tree */
23}
24
25/* --------------------------------------------------- *
26 * -- uninitialized global data (static structures) -- *
27 * --------------------------------------------------- */
28
29var sltree = new Tree();
30var sdtree = new Tree();
31
32/* extra bits and base tables for length codes */
33var length_bits = new Uint8Array(30);
34var length_base = new Uint16Array(30);
35
36/* extra bits and base tables for distance codes */
37var dist_bits = new Uint8Array(30);
38var dist_base = new Uint16Array(30);
39
40/* special ordering of code length codes */
41var clcidx = new Uint8Array([
42  16, 17, 18, 0, 8, 7, 9, 6,
43  10, 5, 11, 4, 12, 3, 13, 2,
44  14, 1, 15
45]);
46
47/* used by tinf_decode_trees, avoids allocations every call */
48var code_tree = new Tree();
49var lengths = new Uint8Array(288 + 32);
50
51/* ----------------------- *
52 * -- utility functions -- *
53 * ----------------------- */
54
55/* build extra bits and base tables */
56function tinf_build_bits_base(bits, base, delta, first) {
57  var i, sum;
58
59  /* build bits table */
60  for (i = 0; i < delta; ++i) bits[i] = 0;
61  for (i = 0; i < 30 - delta; ++i) bits[i + delta] = i / delta | 0;
62
63  /* build base table */
64  for (sum = first, i = 0; i < 30; ++i) {
65    base[i] = sum;
66    sum += 1 << bits[i];
67  }
68}
69
70/* build the fixed huffman trees */
71function tinf_build_fixed_trees(lt, dt) {
72  var i;
73
74  /* build fixed length tree */
75  for (i = 0; i < 7; ++i) lt.table[i] = 0;
76
77  lt.table[7] = 24;
78  lt.table[8] = 152;
79  lt.table[9] = 112;
80
81  for (i = 0; i < 24; ++i) lt.trans[i] = 256 + i;
82  for (i = 0; i < 144; ++i) lt.trans[24 + i] = i;
83  for (i = 0; i < 8; ++i) lt.trans[24 + 144 + i] = 280 + i;
84  for (i = 0; i < 112; ++i) lt.trans[24 + 144 + 8 + i] = 144 + i;
85
86  /* build fixed distance tree */
87  for (i = 0; i < 5; ++i) dt.table[i] = 0;
88
89  dt.table[5] = 32;
90
91  for (i = 0; i < 32; ++i) dt.trans[i] = i;
92}
93
94/* given an array of code lengths, build a tree */
95var offs = new Uint16Array(16);
96
97function tinf_build_tree(t, lengths, off, num) {
98  var i, sum;
99
100  /* clear code length count table */
101  for (i = 0; i < 16; ++i) t.table[i] = 0;
102
103  /* scan symbol lengths, and sum code length counts */
104  for (i = 0; i < num; ++i) t.table[lengths[off + i]]++;
105
106  t.table[0] = 0;
107
108  /* compute offset table for distribution sort */
109  for (sum = 0, i = 0; i < 16; ++i) {
110    offs[i] = sum;
111    sum += t.table[i];
112  }
113
114  /* create code->symbol translation table (symbols sorted by code) */
115  for (i = 0; i < num; ++i) {
116    if (lengths[off + i]) t.trans[offs[lengths[off + i]]++] = i;
117  }
118}
119
120/* ---------------------- *
121 * -- decode functions -- *
122 * ---------------------- */
123
124/* get one bit from source stream */
125function tinf_getbit(d) {
126  /* check if tag is empty */
127  if (!d.bitcount--) {
128    /* load next tag */
129    d.tag = d.source[d.sourceIndex++];
130    d.bitcount = 7;
131  }
132
133  /* shift bit out of tag */
134  var bit = d.tag & 1;
135  d.tag >>>= 1;
136
137  return bit;
138}
139
140/* read a num bit value from a stream and add base */
141function tinf_read_bits(d, num, base) {
142  if (!num)
143    return base;
144
145  while (d.bitcount < 24) {
146    d.tag |= d.source[d.sourceIndex++] << d.bitcount;
147    d.bitcount += 8;
148  }
149
150  var val = d.tag & (0xffff >>> (16 - num));
151  d.tag >>>= num;
152  d.bitcount -= num;
153  return val + base;
154}
155
156/* given a data stream and a tree, decode a symbol */
157function tinf_decode_symbol(d, t) {
158  while (d.bitcount < 24) {
159    d.tag |= d.source[d.sourceIndex++] << d.bitcount;
160    d.bitcount += 8;
161  }
162  
163  var sum = 0, cur = 0, len = 0;
164  var tag = d.tag;
165
166  /* get more bits while code value is above sum */
167  do {
168    cur = 2 * cur + (tag & 1);
169    tag >>>= 1;
170    ++len;
171
172    sum += t.table[len];
173    cur -= t.table[len];
174  } while (cur >= 0);
175  
176  d.tag = tag;
177  d.bitcount -= len;
178
179  return t.trans[sum + cur];
180}
181
182/* given a data stream, decode dynamic trees from it */
183function tinf_decode_trees(d, lt, dt) {
184  var hlit, hdist, hclen;
185  var i, num, length;
186
187  /* get 5 bits HLIT (257-286) */
188  hlit = tinf_read_bits(d, 5, 257);
189
190  /* get 5 bits HDIST (1-32) */
191  hdist = tinf_read_bits(d, 5, 1);
192
193  /* get 4 bits HCLEN (4-19) */
194  hclen = tinf_read_bits(d, 4, 4);
195
196  for (i = 0; i < 19; ++i) lengths[i] = 0;
197
198  /* read code lengths for code length alphabet */
199  for (i = 0; i < hclen; ++i) {
200    /* get 3 bits code length (0-7) */
201    var clen = tinf_read_bits(d, 3, 0);
202    lengths[clcidx[i]] = clen;
203  }
204
205  /* build code length tree */
206  tinf_build_tree(code_tree, lengths, 0, 19);
207
208  /* decode code lengths for the dynamic trees */
209  for (num = 0; num < hlit + hdist;) {
210    var sym = tinf_decode_symbol(d, code_tree);
211
212    switch (sym) {
213      case 16:
214        /* copy previous code length 3-6 times (read 2 bits) */
215        var prev = lengths[num - 1];
216        for (length = tinf_read_bits(d, 2, 3); length; --length) {
217          lengths[num++] = prev;
218        }
219        break;
220      case 17:
221        /* repeat code length 0 for 3-10 times (read 3 bits) */
222        for (length = tinf_read_bits(d, 3, 3); length; --length) {
223          lengths[num++] = 0;
224        }
225        break;
226      case 18:
227        /* repeat code length 0 for 11-138 times (read 7 bits) */
228        for (length = tinf_read_bits(d, 7, 11); length; --length) {
229          lengths[num++] = 0;
230        }
231        break;
232      default:
233        /* values 0-15 represent the actual code lengths */
234        lengths[num++] = sym;
235        break;
236    }
237  }
238
239  /* build dynamic trees */
240  tinf_build_tree(lt, lengths, 0, hlit);
241  tinf_build_tree(dt, lengths, hlit, hdist);
242}
243
244/* ----------------------------- *
245 * -- block inflate functions -- *
246 * ----------------------------- */
247
248/* given a stream and two trees, inflate a block of data */
249function tinf_inflate_block_data(d, lt, dt) {
250  while (1) {
251    var sym = tinf_decode_symbol(d, lt);
252
253    /* check for end of block */
254    if (sym === 256) {
255      return TINF_OK;
256    }
257
258    if (sym < 256) {
259      d.dest[d.destLen++] = sym;
260    } else {
261      var length, dist, offs;
262      var i;
263
264      sym -= 257;
265
266      /* possibly get more bits from length code */
267      length = tinf_read_bits(d, length_bits[sym], length_base[sym]);
268
269      dist = tinf_decode_symbol(d, dt);
270
271      /* possibly get more bits from distance code */
272      offs = d.destLen - tinf_read_bits(d, dist_bits[dist], dist_base[dist]);
273
274      /* copy match */
275      for (i = offs; i < offs + length; ++i) {
276        d.dest[d.destLen++] = d.dest[i];
277      }
278    }
279  }
280}
281
282/* inflate an uncompressed block of data */
283function tinf_inflate_uncompressed_block(d) {
284  var length, invlength;
285  var i;
286  
287  /* unread from bitbuffer */
288  while (d.bitcount > 8) {
289    d.sourceIndex--;
290    d.bitcount -= 8;
291  }
292
293  /* get length */
294  length = d.source[d.sourceIndex + 1];
295  length = 256 * length + d.source[d.sourceIndex];
296
297  /* get one's complement of length */
298  invlength = d.source[d.sourceIndex + 3];
299  invlength = 256 * invlength + d.source[d.sourceIndex + 2];
300
301  /* check length */
302  if (length !== (~invlength & 0x0000ffff))
303    return TINF_DATA_ERROR;
304
305  d.sourceIndex += 4;
306
307  /* copy block */
308  for (i = length; i; --i)
309    d.dest[d.destLen++] = d.source[d.sourceIndex++];
310
311  /* make sure we start next block on a byte boundary */
312  d.bitcount = 0;
313
314  return TINF_OK;
315}
316
317/* inflate stream from source to dest */
318function tinf_uncompress(source, dest) {
319  var d = new Data(source, dest);
320  var bfinal, btype, res;
321
322  do {
323    /* read final block flag */
324    bfinal = tinf_getbit(d);
325
326    /* read block type (2 bits) */
327    btype = tinf_read_bits(d, 2, 0);
328
329    /* decompress block */
330    switch (btype) {
331      case 0:
332        /* decompress uncompressed block */
333        res = tinf_inflate_uncompressed_block(d);
334        break;
335      case 1:
336        /* decompress block with fixed huffman trees */
337        res = tinf_inflate_block_data(d, sltree, sdtree);
338        break;
339      case 2:
340        /* decompress block with dynamic huffman trees */
341        tinf_decode_trees(d, d.ltree, d.dtree);
342        res = tinf_inflate_block_data(d, d.ltree, d.dtree);
343        break;
344      default:
345        res = TINF_DATA_ERROR;
346    }
347
348    if (res !== TINF_OK)
349      throw new Error('Data error');
350
351  } while (!bfinal);
352
353  if (d.destLen < d.dest.length) {
354    if (typeof d.dest.slice === 'function')
355      return d.dest.slice(0, d.destLen);
356    else
357      return d.dest.subarray(0, d.destLen);
358  }
359  
360  return d.dest;
361}
362
363/* -------------------- *
364 * -- initialization -- *
365 * -------------------- */
366
367/* build fixed huffman trees */
368tinf_build_fixed_trees(sltree, sdtree);
369
370/* build extra bits and base tables */
371tinf_build_bits_base(length_bits, length_base, 4, 3);
372tinf_build_bits_base(dist_bits, dist_base, 2, 1);
373
374/* fix a special case */
375length_bits[28] = 0;
376length_base[28] = 258;
377
378export default tinf_uncompress;
379