Skip to content

Commit 6c52c29

Browse files
🍱 Add Deno serve QR server example (#19)
1 parent db197fd commit 6c52c29

3 files changed

Lines changed: 221 additions & 1 deletion

File tree

‎README.md‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ This is a port of [zenozeng/node-yaqrcode](https://github.com/zenozeng/node-yaqr
1616

1717
```ts
1818
import { qrcode } from "https://deno.land/x/qrcode/mod.ts";
19-
const base64Image = qrcode("bitcoin:ADDRESS?amount=0.5&label=ORDER"); // data:image/gif;base64,...
19+
const base64Image = await qrcode("bitcoin:ADDRESS?amount=0.5&label=ORDER"); // data:image/gif;base64,...
2020
```
2121

2222
![QR code](https://upload.wikimedia.org/wikipedia/commons/thumb/d/d0/QR_code_for_mobile_English_Wikipedia.svg/240px-QR_code_for_mobile_English_Wikipedia.svg.png)
@@ -26,6 +26,23 @@ You can also add a custom size by specifying `size` in the second parameter:
2626
```ts
2727
const fixedSizeImage = await qrcode("bitcoin:ADDRESS?amount=0.5&label=ORDER", { size: 500 });
2828
```
29+
30+
### Deno.serve example
31+
32+
The `examples/server.ts` file exposes QR codes over HTTP using `Deno.serve`:
33+
34+
```bash
35+
deno run --allow-net examples/server.ts
36+
```
37+
38+
Then open a URL like this to receive a GIF response:
39+
40+
```text
41+
http://localhost:4000/?data=Hello%20World!&size=250&typeNumber=5&errorCorrection=L
42+
```
43+
44+
Short query parameters are also supported for compact URLs: `d`, `s`, `v`, and `e`.
45+
2946
### CLI with [DPX](https://github.com/denorg/dpx)
3047

3148
After [installing DPX](https://github.com/denorg/dpx), you can directly use the CLI using the `dpx` command:

‎examples/server.ts‎

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { qrcode } from "../mod.ts";
2+
3+
const GIF_DATA_URL_PREFIX = "data:image/gif;base64,";
4+
const TYPE_NUMBERS = [
5+
0,
6+
1,
7+
2,
8+
3,
9+
4,
10+
5,
11+
6,
12+
7,
13+
8,
14+
9,
15+
10,
16+
11,
17+
12,
18+
13,
19+
14,
20+
15,
21+
16,
22+
17,
23+
18,
24+
19,
25+
20,
26+
21,
27+
22,
28+
23,
29+
24,
30+
25,
31+
26,
32+
27,
33+
28,
34+
29,
35+
30,
36+
31,
37+
32,
38+
33,
39+
34,
40+
35,
41+
36,
42+
37,
43+
38,
44+
39,
45+
40,
46+
] as const;
47+
const ERROR_CORRECTION_LEVELS = ["L", "M", "Q", "H"] as const;
48+
49+
type TypeNumberOption = (typeof TYPE_NUMBERS)[number];
50+
type ErrorCorrectionLevelOption = (typeof ERROR_CORRECTION_LEVELS)[number];
51+
52+
function parseIntegerParam(
53+
value: string | null,
54+
fallback: number,
55+
min: number,
56+
max: number,
57+
): number {
58+
if (value === null || value.trim() === "") return fallback;
59+
60+
const parsed = Number(value);
61+
62+
if (!Number.isInteger(parsed)) return fallback;
63+
64+
return Math.min(max, Math.max(min, parsed));
65+
}
66+
67+
function parseTypeNumber(value: string | null): TypeNumberOption {
68+
return TYPE_NUMBERS[parseIntegerParam(value, 4, 0, 40)] ?? 4;
69+
}
70+
71+
function parseErrorCorrectionLevel(
72+
value: string | null,
73+
): ErrorCorrectionLevelOption {
74+
return ERROR_CORRECTION_LEVELS.includes(value as ErrorCorrectionLevelOption)
75+
? value as ErrorCorrectionLevelOption
76+
: "M";
77+
}
78+
79+
function dataUrlToBytes(dataUrl: string): Uint8Array {
80+
if (!dataUrl.startsWith(GIF_DATA_URL_PREFIX)) {
81+
throw new Error("Expected qrcode() to return a GIF data URL");
82+
}
83+
84+
return Uint8Array.from(
85+
atob(dataUrl.slice(GIF_DATA_URL_PREFIX.length)),
86+
(character) => character.charCodeAt(0),
87+
);
88+
}
89+
90+
const commonHeaders = {
91+
"access-control-allow-origin": "*",
92+
"access-control-allow-methods": "GET, HEAD, OPTIONS",
93+
"cache-control": "no-store",
94+
};
95+
96+
export async function handleRequest(request: Request): Promise<Response> {
97+
if (request.method === "OPTIONS") {
98+
return new Response(null, {
99+
status: 204,
100+
headers: commonHeaders,
101+
});
102+
}
103+
104+
if (request.method !== "GET" && request.method !== "HEAD") {
105+
return new Response("Method not allowed", {
106+
status: 405,
107+
headers: {
108+
...commonHeaders,
109+
allow: "GET, HEAD, OPTIONS",
110+
"content-type": "text/plain; charset=utf-8",
111+
},
112+
});
113+
}
114+
115+
const url = new URL(request.url);
116+
const data = url.searchParams.get("data") ?? url.searchParams.get("d") ??
117+
"Hello World!";
118+
const size = parseIntegerParam(
119+
url.searchParams.get("size") ?? url.searchParams.get("s"),
120+
250,
121+
100,
122+
1000,
123+
);
124+
const typeNumber = parseTypeNumber(
125+
url.searchParams.get("typeNumber") ?? url.searchParams.get("v"),
126+
);
127+
const errorCorrectLevel = parseErrorCorrectionLevel(
128+
url.searchParams.get("errorCorrection") ?? url.searchParams.get("e"),
129+
);
130+
131+
const dataUrl = await qrcode(data, {
132+
errorCorrectLevel,
133+
size,
134+
typeNumber,
135+
});
136+
const gif = dataUrlToBytes(dataUrl);
137+
138+
return new Response(request.method === "HEAD" ? null : gif, {
139+
status: 200,
140+
headers: {
141+
...commonHeaders,
142+
"content-length": String(gif.byteLength),
143+
"content-type": "image/gif",
144+
},
145+
});
146+
}
147+
148+
if (import.meta.main) {
149+
Deno.serve({ port: 4000 }, handleRequest);
150+
}

‎examples/server_test.ts‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {
2+
assert,
3+
assertEquals,
4+
} from "https://deno.land/std@0.224.0/testing/asserts.ts";
5+
import { handleRequest } from "./server.ts";
6+
7+
Deno.test("server example returns a GIF response", async (): Promise<void> => {
8+
const response = await handleRequest(
9+
new Request("http://localhost/?d=Hello%20World%21&v=5&e=L&s=250"),
10+
);
11+
const bytes = new Uint8Array(await response.arrayBuffer());
12+
13+
assertEquals(response.status, 200);
14+
assertEquals(response.headers.get("content-type"), "image/gif");
15+
assertEquals(new TextDecoder().decode(bytes.slice(0, 6)), "GIF87a");
16+
assert(bytes.byteLength > 0);
17+
});
18+
19+
Deno.test("server example handles HEAD without a body", async (): Promise<void> => {
20+
const response = await handleRequest(
21+
new Request("http://localhost/?data=Hello", { method: "HEAD" }),
22+
);
23+
24+
assertEquals(response.status, 200);
25+
assertEquals(response.headers.get("content-type"), "image/gif");
26+
assertEquals((await response.arrayBuffer()).byteLength, 0);
27+
});
28+
29+
Deno.test("server example falls back when optional query params are missing or blank", async (): Promise<void> => {
30+
const [defaults, explicitFallbacks] = await Promise.all([
31+
handleRequest(new Request("http://localhost/?data=Hello&s=&v=&e=bad")),
32+
handleRequest(
33+
new Request(
34+
"http://localhost/?data=Hello&size=250&typeNumber=4&errorCorrection=M",
35+
),
36+
),
37+
]);
38+
39+
assertEquals(defaults.status, 200);
40+
assertEquals(
41+
new Uint8Array(await defaults.arrayBuffer()),
42+
new Uint8Array(await explicitFallbacks.arrayBuffer()),
43+
);
44+
});
45+
46+
Deno.test("server example rejects unsupported methods", async (): Promise<void> => {
47+
const response = await handleRequest(
48+
new Request("http://localhost/", { method: "POST" }),
49+
);
50+
51+
assertEquals(response.status, 405);
52+
assertEquals(response.headers.get("allow"), "GET, HEAD, OPTIONS");
53+
});

0 commit comments

Comments
 (0)