|
| 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 | +} |
0 commit comments