@danieltmn/openbridge 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +146 -143
- package/package.json +9 -1
- package/src/bridge/bridge.js +85 -1
- package/src/cli.js +92 -2
- package/src/qr.js +224 -0
- package/src/web/assets/app.js +358 -9
- package/src/web/routes.js +3 -0
- package/src/web/templates/chat.html +57 -1
package/src/qr.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generador de QR minimo (ISO/IEC 18004, Model 2) sin dependencias.
|
|
5
|
+
*
|
|
6
|
+
* Soporta modo byte (UTF-8) con correccion de errores nivel L, versiones 1 a 5
|
|
7
|
+
* (hasta 106 bytes, de sobra para una URL de tunel) y un solo bloque RS. La
|
|
8
|
+
* mascara es fija (0): cualquier mascara es valida y la info de formato la
|
|
9
|
+
* declara, asi que el resultado es escaneable.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// dataCodewords, eccCodewords y centros de patrones de alineacion por version.
|
|
13
|
+
const VERSIONS = {
|
|
14
|
+
1: { data: 19, ecc: 7, align: [] },
|
|
15
|
+
2: { data: 34, ecc: 10, align: [6, 18] },
|
|
16
|
+
3: { data: 55, ecc: 15, align: [6, 22] },
|
|
17
|
+
4: { data: 80, ecc: 20, align: [6, 26] },
|
|
18
|
+
5: { data: 108, ecc: 26, align: [6, 30] },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const MASK = 0;
|
|
22
|
+
const ECC_FORMAT_BITS = 1; // nivel L -> 01
|
|
23
|
+
|
|
24
|
+
function gfMul(x, y) {
|
|
25
|
+
let z = 0;
|
|
26
|
+
for (let i = 7; i >= 0; i--) {
|
|
27
|
+
z = (z << 1) ^ ((z >>> 7) * 0x11d);
|
|
28
|
+
z ^= ((y >>> i) & 1) * x;
|
|
29
|
+
}
|
|
30
|
+
return z & 0xff;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function rsDivisor(degree) {
|
|
34
|
+
const result = new Array(degree).fill(0);
|
|
35
|
+
result[degree - 1] = 1;
|
|
36
|
+
let root = 1;
|
|
37
|
+
for (let i = 0; i < degree; i++) {
|
|
38
|
+
for (let j = 0; j < result.length; j++) {
|
|
39
|
+
result[j] = gfMul(result[j], root);
|
|
40
|
+
if (j + 1 < result.length) result[j] ^= result[j + 1];
|
|
41
|
+
}
|
|
42
|
+
root = gfMul(root, 0x02);
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function rsRemainder(data, divisor) {
|
|
48
|
+
const result = divisor.map(() => 0);
|
|
49
|
+
for (const b of data) {
|
|
50
|
+
const factor = b ^ result.shift();
|
|
51
|
+
result.push(0);
|
|
52
|
+
for (let i = 0; i < divisor.length; i++) result[i] ^= gfMul(divisor[i], factor);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getBit(x, i) {
|
|
58
|
+
return ((x >>> i) & 1) !== 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function chooseVersion(byteLen) {
|
|
62
|
+
for (let v = 1; v <= 5; v++) {
|
|
63
|
+
if (byteLen <= VERSIONS[v].data - 2) return v;
|
|
64
|
+
}
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function dataCodewords(bytes, version) {
|
|
69
|
+
const cap = VERSIONS[version].data;
|
|
70
|
+
const bits = [];
|
|
71
|
+
const push = (val, len) => { for (let i = len - 1; i >= 0; i--) bits.push((val >>> i) & 1); };
|
|
72
|
+
push(0b0100, 4); // modo byte
|
|
73
|
+
push(bytes.length, 8); // conteo (v1-9: 8 bits)
|
|
74
|
+
for (const b of bytes) push(b, 8);
|
|
75
|
+
for (let i = 0; i < 4 && bits.length < cap * 8; i++) bits.push(0);
|
|
76
|
+
while (bits.length % 8 !== 0) bits.push(0);
|
|
77
|
+
const out = [];
|
|
78
|
+
for (let i = 0; i < bits.length; i += 8) {
|
|
79
|
+
let b = 0;
|
|
80
|
+
for (let j = 0; j < 8; j++) b = (b << 1) | bits[i + j];
|
|
81
|
+
out.push(b);
|
|
82
|
+
}
|
|
83
|
+
const pad = [0xec, 0x11];
|
|
84
|
+
let k = 0;
|
|
85
|
+
while (out.length < cap) out.push(pad[k++ % 2]);
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildMatrix(version, codewords) {
|
|
90
|
+
const size = version * 4 + 17;
|
|
91
|
+
const modules = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
92
|
+
const isFn = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
93
|
+
const set = (x, y, dark) => { modules[y][x] = dark; isFn[y][x] = true; };
|
|
94
|
+
|
|
95
|
+
// Patrones de sincronizacion.
|
|
96
|
+
for (let i = 0; i < size; i++) {
|
|
97
|
+
set(6, i, i % 2 === 0);
|
|
98
|
+
set(i, 6, i % 2 === 0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Localizadores (finder) + separadores.
|
|
102
|
+
const drawFinder = (cx, cy) => {
|
|
103
|
+
for (let dy = -4; dy <= 4; dy++) {
|
|
104
|
+
for (let dx = -4; dx <= 4; dx++) {
|
|
105
|
+
const x = cx + dx, y = cy + dy;
|
|
106
|
+
if (x < 0 || x >= size || y < 0 || y >= size) continue;
|
|
107
|
+
const dist = Math.max(Math.abs(dx), Math.abs(dy));
|
|
108
|
+
set(x, y, dist !== 2 && dist !== 4);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
drawFinder(3, 3);
|
|
113
|
+
drawFinder(size - 4, 3);
|
|
114
|
+
drawFinder(3, size - 4);
|
|
115
|
+
|
|
116
|
+
// Patrones de alineacion.
|
|
117
|
+
const pos = VERSIONS[version].align;
|
|
118
|
+
for (const cy of pos) {
|
|
119
|
+
for (const cx of pos) {
|
|
120
|
+
if ((cx === 6 && cy === 6) || (cx === 6 && cy === size - 7) || (cx === size - 7 && cy === 6)) continue;
|
|
121
|
+
for (let dy = -2; dy <= 2; dy++) {
|
|
122
|
+
for (let dx = -2; dx <= 2; dx++) {
|
|
123
|
+
set(cx + dx, cy + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Reserva la info de formato (se reescribe abajo) y el modulo oscuro.
|
|
130
|
+
drawFormatBits(size, modules, isFn, 0);
|
|
131
|
+
|
|
132
|
+
// Colocacion de datos en zigzag.
|
|
133
|
+
let i = 0;
|
|
134
|
+
for (let right = size - 1; right >= 1; right -= 2) {
|
|
135
|
+
if (right === 6) right = 5;
|
|
136
|
+
for (let vert = 0; vert < size; vert++) {
|
|
137
|
+
for (let j = 0; j < 2; j++) {
|
|
138
|
+
const x = right - j;
|
|
139
|
+
const upward = ((right + 1) & 2) === 0;
|
|
140
|
+
const y = upward ? size - 1 - vert : vert;
|
|
141
|
+
if (isFn[y][x] || i >= codewords.length * 8) continue;
|
|
142
|
+
modules[y][x] = getBit(codewords[i >>> 3], 7 - (i & 7));
|
|
143
|
+
i++;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Mascara fija.
|
|
149
|
+
for (let y = 0; y < size; y++) {
|
|
150
|
+
for (let x = 0; x < size; x++) {
|
|
151
|
+
if (!isFn[y][x] && (x + y) % 2 === 0) modules[y][x] = !modules[y][x];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
drawFormatBits(size, modules, isFn, MASK);
|
|
156
|
+
return { size, modules };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function drawFormatBits(size, modules, isFn, mask) {
|
|
160
|
+
const data = (ECC_FORMAT_BITS << 3) | mask;
|
|
161
|
+
let rem = data;
|
|
162
|
+
for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
|
|
163
|
+
const bits = ((data << 10) | rem) ^ 0x5412;
|
|
164
|
+
const put = (x, y, dark) => { modules[y][x] = dark; isFn[y][x] = true; };
|
|
165
|
+
|
|
166
|
+
for (let i = 0; i <= 5; i++) put(8, i, getBit(bits, i));
|
|
167
|
+
put(8, 7, getBit(bits, 6));
|
|
168
|
+
put(8, 8, getBit(bits, 7));
|
|
169
|
+
put(7, 8, getBit(bits, 8));
|
|
170
|
+
for (let i = 9; i < 15; i++) put(14 - i, 8, getBit(bits, i));
|
|
171
|
+
|
|
172
|
+
for (let i = 0; i < 8; i++) put(size - 1 - i, 8, getBit(bits, i));
|
|
173
|
+
for (let i = 8; i < 15; i++) put(8, size - 15 + i, getBit(bits, i));
|
|
174
|
+
put(8, size - 8, true);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Devuelve { size, modules } o null si el texto no entra (URL muy larga).
|
|
178
|
+
function qrMatrix(text) {
|
|
179
|
+
const bytes = Buffer.from(String(text), 'utf8');
|
|
180
|
+
const version = chooseVersion(bytes.length);
|
|
181
|
+
if (!version) return null;
|
|
182
|
+
const data = dataCodewords(bytes, version);
|
|
183
|
+
const ecc = rsRemainder(data, rsDivisor(VERSIONS[version].ecc));
|
|
184
|
+
return buildMatrix(version, data.concat(ecc));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// QR para terminal con medios bloques (cuadrado en consolas 1:2).
|
|
188
|
+
function qrTerminal(text, quiet) {
|
|
189
|
+
const qr = qrMatrix(text);
|
|
190
|
+
if (!qr) return '';
|
|
191
|
+
const q = quiet === undefined ? 2 : quiet;
|
|
192
|
+
const size = qr.size;
|
|
193
|
+
const at = (x, y) => (x >= 0 && x < size && y >= 0 && y < size) && qr.modules[y][x];
|
|
194
|
+
const lines = [];
|
|
195
|
+
for (let y = -q; y < size + q; y += 2) {
|
|
196
|
+
let line = '';
|
|
197
|
+
for (let x = -q; x < size + q; x++) {
|
|
198
|
+
const top = at(x, y), bot = at(x, y + 1);
|
|
199
|
+
line += top && bot ? '\u2588' : top ? '\u2580' : bot ? '\u2584' : ' ';
|
|
200
|
+
}
|
|
201
|
+
lines.push(line);
|
|
202
|
+
}
|
|
203
|
+
return lines.join('\n');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// QR como SVG (para la web o para guardar).
|
|
207
|
+
function qrSvg(text, quiet) {
|
|
208
|
+
const qr = qrMatrix(text);
|
|
209
|
+
if (!qr) return '';
|
|
210
|
+
const q = quiet === undefined ? 4 : quiet;
|
|
211
|
+
const size = qr.size;
|
|
212
|
+
let path = '';
|
|
213
|
+
for (let y = 0; y < size; y++) {
|
|
214
|
+
for (let x = 0; x < size; x++) {
|
|
215
|
+
if (qr.modules[y][x]) path += 'M' + (x + q) + ' ' + (y + q) + 'h1v1h-1z';
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const total = size + q * 2;
|
|
219
|
+
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + total + ' ' + total
|
|
220
|
+
+ '" shape-rendering="crispEdges"><rect width="' + total + '" height="' + total
|
|
221
|
+
+ '" fill="#fff"/><path d="' + path + '" fill="#000"/></svg>';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = { qrMatrix, qrTerminal, qrSvg };
|