Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This is a port of [zenozeng/node-yaqrcode](https://github.com/zenozeng/node-yaqr

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

![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)
Expand All @@ -26,6 +26,23 @@ You can also add a custom size by specifying `size` in the second parameter:
```ts
const fixedSizeImage = await qrcode("bitcoin:ADDRESS?amount=0.5&label=ORDER", { size: 500 });
```

### Deno.serve example

The `examples/server.ts` file exposes QR codes over HTTP using `Deno.serve`:

```bash
deno run --allow-net examples/server.ts
```

Then open a URL like this to receive a GIF response:

```text
http://localhost:4000/?data=Hello%20World!&size=250&typeNumber=5&errorCorrection=L
```

Short query parameters are also supported for compact URLs: `d`, `s`, `v`, and `e`.

### CLI with [DPX](https://github.com/denorg/dpx)

After [installing DPX](https://github.com/denorg/dpx), you can directly use the CLI using the `dpx` command:
Expand Down
150 changes: 150 additions & 0 deletions examples/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { qrcode } from "../mod.ts";

const GIF_DATA_URL_PREFIX = "data:image/gif;base64,";
const TYPE_NUMBERS = [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
] as const;
const ERROR_CORRECTION_LEVELS = ["L", "M", "Q", "H"] as const;

type TypeNumberOption = (typeof TYPE_NUMBERS)[number];
type ErrorCorrectionLevelOption = (typeof ERROR_CORRECTION_LEVELS)[number];

function parseIntegerParam(
value: string | null,
fallback: number,
min: number,
max: number,
): number {
if (value === null || value.trim() === "") return fallback;

const parsed = Number(value);

if (!Number.isInteger(parsed)) return fallback;

return Math.min(max, Math.max(min, parsed));
}

function parseTypeNumber(value: string | null): TypeNumberOption {
return TYPE_NUMBERS[parseIntegerParam(value, 4, 0, 40)] ?? 4;
}

function parseErrorCorrectionLevel(
value: string | null,
): ErrorCorrectionLevelOption {
return ERROR_CORRECTION_LEVELS.includes(value as ErrorCorrectionLevelOption)
? value as ErrorCorrectionLevelOption
: "M";
}

function dataUrlToBytes(dataUrl: string): Uint8Array {
if (!dataUrl.startsWith(GIF_DATA_URL_PREFIX)) {
throw new Error("Expected qrcode() to return a GIF data URL");
}

return Uint8Array.from(
atob(dataUrl.slice(GIF_DATA_URL_PREFIX.length)),
(character) => character.charCodeAt(0),
);
}

const commonHeaders = {
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET, HEAD, OPTIONS",
"cache-control": "no-store",
};

export async function handleRequest(request: Request): Promise<Response> {
if (request.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: commonHeaders,
});
}

if (request.method !== "GET" && request.method !== "HEAD") {
return new Response("Method not allowed", {
status: 405,
headers: {
...commonHeaders,
allow: "GET, HEAD, OPTIONS",
"content-type": "text/plain; charset=utf-8",
},
});
}

const url = new URL(request.url);
const data = url.searchParams.get("data") ?? url.searchParams.get("d") ??
"Hello World!";
const size = parseIntegerParam(
url.searchParams.get("size") ?? url.searchParams.get("s"),
250,
100,
1000,
);
const typeNumber = parseTypeNumber(
url.searchParams.get("typeNumber") ?? url.searchParams.get("v"),
);
const errorCorrectLevel = parseErrorCorrectionLevel(
url.searchParams.get("errorCorrection") ?? url.searchParams.get("e"),
);

const dataUrl = await qrcode(data, {
errorCorrectLevel,
size,
typeNumber,
});
const gif = dataUrlToBytes(dataUrl);

return new Response(request.method === "HEAD" ? null : gif, {
status: 200,
headers: {
...commonHeaders,
"content-length": String(gif.byteLength),
"content-type": "image/gif",
},
});
}

if (import.meta.main) {
Deno.serve({ port: 4000 }, handleRequest);
}
53 changes: 53 additions & 0 deletions examples/server_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
assert,
assertEquals,
} from "https://deno.land/std@0.224.0/testing/asserts.ts";
import { handleRequest } from "./server.ts";

Deno.test("server example returns a GIF response", async (): Promise<void> => {
const response = await handleRequest(
new Request("http://localhost/?d=Hello%20World%21&v=5&e=L&s=250"),
);
const bytes = new Uint8Array(await response.arrayBuffer());

assertEquals(response.status, 200);
assertEquals(response.headers.get("content-type"), "image/gif");
assertEquals(new TextDecoder().decode(bytes.slice(0, 6)), "GIF87a");
assert(bytes.byteLength > 0);
});

Deno.test("server example handles HEAD without a body", async (): Promise<void> => {
const response = await handleRequest(
new Request("http://localhost/?data=Hello", { method: "HEAD" }),
);

assertEquals(response.status, 200);
assertEquals(response.headers.get("content-type"), "image/gif");
assertEquals((await response.arrayBuffer()).byteLength, 0);
});

Deno.test("server example falls back when optional query params are missing or blank", async (): Promise<void> => {
const [defaults, explicitFallbacks] = await Promise.all([
handleRequest(new Request("http://localhost/?data=Hello&s=&v=&e=bad")),
handleRequest(
new Request(
"http://localhost/?data=Hello&size=250&typeNumber=4&errorCorrection=M",
),
),
]);

assertEquals(defaults.status, 200);
assertEquals(
new Uint8Array(await defaults.arrayBuffer()),
new Uint8Array(await explicitFallbacks.arrayBuffer()),
);
});

Deno.test("server example rejects unsupported methods", async (): Promise<void> => {
const response = await handleRequest(
new Request("http://localhost/", { method: "POST" }),
);

assertEquals(response.status, 405);
assertEquals(response.headers.get("allow"), "GET, HEAD, OPTIONS");
});
Loading