feat: add 'mini' (double quad-blocks) ascii QR output
[crowdnode.js/.git] / lib / qr.js
1 "use strict";
2
3 let Qr = module.exports;
4
5 let Fs = require("fs").promises;
6
7 let QrCode = require("qrcode-svg");
8
9 /**
10  * @typedef QrOpts
11  * @property {String} [background]
12  * @property {String} [color]
13  * @property {String} [ecl]
14  * @property {Number} [height]
15  * @property {Number} [indent]
16  * @property {Number} [padding]
17  * @property {"mini" | "micro"} [size]
18  * @property {Number} [width]
19  */
20
21 /**
22  * @param {String} data
23  * @param {QrOpts} opts
24  */
25 Qr._create = function (data, opts) {
26   return new QrCode({
27     content: data,
28     padding: opts?.padding || 4,
29     width: opts?.width || 256,
30     height: opts?.height || 256,
31     color: opts?.color || "#000000",
32     background: opts?.background || "#ffffff",
33     ecl: opts?.ecl || "M",
34   });
35 };
36
37 /**
38  * @typedef {Object.<String, String>} BlockMap
39  */
40
41 /**
42  * Encoded as top-left, top-right, bottom-left, bottom-right
43  * @type {Object.<"mini" | "micro", BlockMap>}
44  */
45 let charMaps = {
46   micro: {
47     0b0000: " ",
48     0b0001: "▗",
49     0b0010: "▖",
50     0b0011: "▄",
51     0b0100: "▝",
52     0b0101: "▐",
53     0b0110: "▞",
54     0b0111: "▟",
55     0b1000: "▘",
56     0b1001: "▚",
57     0b1010: "▌",
58     0b1011: "▙",
59     0b1100: "▀",
60     0b1101: "▜",
61     0b1110: "▛",
62     0b1111: "█",
63   },
64   mini: {
65     0b0000: "  ",
66     0b0001: " ▄",
67     0b0010: "▄ ",
68     0b0011: "▄▄",
69     0b0100: " ▀",
70     0b0101: " █",
71     0b0110: "▄▀",
72     0b0111: "▄█",
73     0b1000: "▀ ",
74     0b1001: "▀▄",
75     0b1010: "█ ",
76     0b1011: "█▄",
77     0b1100: "▀▀",
78     0b1101: "▀█",
79     0b1110: "█▀",
80     0b1111: "██",
81   },
82 };
83
84 /**
85  * @param {String} data
86  * @param {QrOpts} opts
87  */
88 Qr.quadAscii = function (data, opts) {
89   let charMap = charMaps[opts.size || "mini"];
90   let qrcode = Qr._create(data, opts);
91   let indent = opts?.indent ?? 4;
92   let modules = qrcode.qrcode.modules;
93
94   let ascii = ``.padStart(indent - 1, " ");
95   let length = modules.length;
96   for (let y = 0; y < length; y += 2) {
97     for (let x = 0; x < length; x += 2) {
98       let count = 0;
99       // qr codes can be odd numbers
100       if (x >= length) {
101         ascii += charMap[count];
102         continue;
103       }
104       if (modules[x][y]) {
105         count += 8;
106       }
107       if (modules[x][y + 1]) {
108         count += 2;
109       }
110
111       if (x + 1 >= length) {
112         ascii += charMap[count];
113         continue;
114       }
115       if (modules[x + 1][y]) {
116         count += 4;
117       }
118       if (modules[x + 1][y + 1]) {
119         count += 1;
120       }
121       ascii += charMap[count];
122     }
123     ascii += `\n`.padEnd(indent, " ");
124   }
125   return ascii.replace(/\s+$/, "");
126 };
127
128 /**
129  * @param {String} data
130  * @param {QrOpts} opts
131  */
132 Qr.ascii = function (data, opts) {
133   if (opts.size) {
134     return Qr.quadAscii(data, opts);
135   }
136
137   let qrcode = Qr._create(data, opts);
138   let indent = opts?.indent ?? 4;
139   let modules = qrcode.qrcode.modules;
140
141   let ascii = ``.padStart(indent - 1, " ");
142   let length = modules.length;
143   for (let y = 0; y < length; y += 1) {
144     for (let x = 0; x < length; x += 1) {
145       let block = "  ";
146       if (modules[x][y]) {
147         block = "██";
148       }
149       ascii += block;
150     }
151     ascii += `\n`.padEnd(indent, " ");
152   }
153   return ascii;
154 };
155
156 /**
157  * @param {String} data
158  * @param {QrOpts} opts
159  */
160 Qr.svg = function (data, opts) {
161   let qrcode = Qr._create(data, opts);
162   return qrcode.svg();
163 };
164
165 /**
166  * @param {String} filepath
167  * @param {String} data
168  * @param {QrOpts} opts
169  */
170 Qr.save = async function (filepath, data, opts) {
171   let qrcode = Qr.svg(data, opts);
172   await Fs.writeFile(filepath, qrcode, "utf8");
173 };