@qrdx/cli 0.0.4 → 0.0.6
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/LICENSE +21 -0
- package/README.md +91 -24
- package/dist/index.js +926 -204
- package/package.json +38 -21
package/dist/index.js
CHANGED
|
@@ -1,39 +1,220 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import { readFileSync, mkdirSync, writeFileSync, createReadStream } from 'fs';
|
|
3
|
+
import { join, resolve, extname } from 'path';
|
|
4
|
+
import { readFile } from 'fs/promises';
|
|
5
|
+
import { BODY_PATTERN, CORNER_EYE_PATTERNS, CORNER_EYE_DOT_PATTERNS, getTemplateIds, FRAME_STYLES, DEFAULT_FRAME_TEXT, FRAME_TEXT_MAX_LENGTH, DEFAULT_MARGIN, getFrameDimensions, colorConfigSchema, errorLevelSchema, QRCodeSVG, svgToEPS } from 'qrdx';
|
|
6
|
+
import { createElement } from 'react';
|
|
7
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
8
|
+
import * as readline from 'readline';
|
|
9
|
+
import * as p from '@clack/prompts';
|
|
3
10
|
|
|
4
|
-
var
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
var CsvParser = class {
|
|
12
|
+
row = [];
|
|
13
|
+
field = "";
|
|
14
|
+
quoted = false;
|
|
15
|
+
/** A quote seen inside a quoted field: the end, or the first of "". */
|
|
16
|
+
quote = false;
|
|
17
|
+
started = false;
|
|
18
|
+
first = true;
|
|
19
|
+
/** Rows completed by this chunk. */
|
|
20
|
+
push(chunk) {
|
|
21
|
+
const rows = [];
|
|
22
|
+
let text2 = chunk;
|
|
23
|
+
if (this.first) {
|
|
24
|
+
this.first = false;
|
|
25
|
+
if (text2.charCodeAt(0) === 65279) text2 = text2.slice(1);
|
|
26
|
+
}
|
|
27
|
+
for (const char of text2) {
|
|
28
|
+
if (this.quoted) {
|
|
29
|
+
if (this.quote) {
|
|
30
|
+
this.quote = false;
|
|
31
|
+
if (char === '"') {
|
|
32
|
+
this.field += '"';
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
this.quoted = false;
|
|
36
|
+
} else if (char === '"') {
|
|
37
|
+
this.quote = true;
|
|
38
|
+
continue;
|
|
39
|
+
} else {
|
|
40
|
+
this.field += char;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
22
43
|
}
|
|
23
|
-
|
|
44
|
+
if (char === '"' && this.field === "" && !this.started) {
|
|
45
|
+
this.quoted = true;
|
|
46
|
+
this.started = true;
|
|
47
|
+
} else if (char === ",") {
|
|
48
|
+
this.endField();
|
|
49
|
+
} else if (char === "\n") {
|
|
50
|
+
this.endRow(rows);
|
|
51
|
+
} else if (char === "\r") ; else {
|
|
52
|
+
this.field += char;
|
|
53
|
+
this.started = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return rows;
|
|
57
|
+
}
|
|
58
|
+
/** The last row, when the file doesn't end with a line break. */
|
|
59
|
+
end() {
|
|
60
|
+
const rows = [];
|
|
61
|
+
if (this.quote) {
|
|
62
|
+
this.quote = false;
|
|
63
|
+
this.quoted = false;
|
|
64
|
+
}
|
|
65
|
+
if (this.started || this.row.length > 0) this.endRow(rows);
|
|
66
|
+
return rows;
|
|
67
|
+
}
|
|
68
|
+
endField() {
|
|
69
|
+
this.row.push(this.field);
|
|
70
|
+
this.field = "";
|
|
71
|
+
this.started = false;
|
|
72
|
+
}
|
|
73
|
+
endRow(rows) {
|
|
74
|
+
this.endField();
|
|
75
|
+
if (this.row.some((cell) => cell.trim() !== "")) rows.push(this.row);
|
|
76
|
+
this.row = [];
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
async function* readCsvRows(path) {
|
|
80
|
+
const parser = new CsvParser();
|
|
81
|
+
const stream = createReadStream(path, { encoding: "utf8" });
|
|
82
|
+
for await (const chunk of stream) {
|
|
83
|
+
yield* parser.push(chunk);
|
|
24
84
|
}
|
|
25
|
-
|
|
26
|
-
return Object.freeze(n);
|
|
85
|
+
yield* parser.end();
|
|
27
86
|
}
|
|
28
|
-
|
|
29
|
-
var
|
|
30
|
-
|
|
87
|
+
var VALUE_COLUMN = /^(value|url|link|data|content|text|qr|qr ?code)$/i;
|
|
88
|
+
var FILENAME_COLUMN = /^(file ?name|name|label|id|title|sku)$/i;
|
|
89
|
+
function guessValueColumn(columns) {
|
|
90
|
+
const index = columns.findIndex((name) => VALUE_COLUMN.test(name.trim()));
|
|
91
|
+
return index === -1 ? 0 : index;
|
|
92
|
+
}
|
|
93
|
+
function guessFilenameColumn(columns, valueColumn) {
|
|
94
|
+
return columns.findIndex(
|
|
95
|
+
(name, i) => i !== valueColumn && FILENAME_COLUMN.test(name.trim())
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
function resolveColumn(spec, columns, flag) {
|
|
99
|
+
if (/^\d+$/.test(spec)) {
|
|
100
|
+
const index = Number(spec) - 1;
|
|
101
|
+
if (index >= 0 && index < columns.length) return index;
|
|
102
|
+
} else {
|
|
103
|
+
const index = columns.findIndex(
|
|
104
|
+
(name) => name.trim().toLowerCase() === spec.trim().toLowerCase()
|
|
105
|
+
);
|
|
106
|
+
if (index >= 0) return index;
|
|
107
|
+
}
|
|
108
|
+
throw new Error(
|
|
109
|
+
`${flag} "${spec}" isn't a column of this file. Columns: ${columns.map((name, i) => `${i + 1}. ${name}`).join(", ")}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
function sanitizeFilename(raw) {
|
|
113
|
+
return raw.normalize("NFKC").replace(/[\u0000-\u001f\u007f<>:"/\\|?*]+/g, "-").replace(/\s+/g, " ").trim().replace(/^[.\-\s]+|[.\s]+$/g, "").slice(0, 100);
|
|
114
|
+
}
|
|
115
|
+
function createFilenameAllocator() {
|
|
116
|
+
const used = /* @__PURE__ */ new Set();
|
|
117
|
+
const next = /* @__PURE__ */ new Map();
|
|
118
|
+
return (base, extension) => {
|
|
119
|
+
const key = base.toLowerCase();
|
|
120
|
+
let n = next.get(key) ?? 1;
|
|
121
|
+
let candidate = n === 1 ? `${base}.${extension}` : `${base}-${n}.${extension}`;
|
|
122
|
+
while (used.has(candidate.toLowerCase())) {
|
|
123
|
+
n++;
|
|
124
|
+
candidate = `${base}-${n}.${extension}`;
|
|
125
|
+
}
|
|
126
|
+
next.set(key, n + 1);
|
|
127
|
+
used.add(candidate.toLowerCase());
|
|
128
|
+
return candidate;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function csvCell(value) {
|
|
132
|
+
const text2 = String(value);
|
|
133
|
+
return /[",\n\r]/.test(text2) ? `"${text2.replaceAll('"', '""')}"` : text2;
|
|
134
|
+
}
|
|
135
|
+
var OUTPUT_FORMATS = [
|
|
136
|
+
"svg",
|
|
137
|
+
"png",
|
|
138
|
+
"webp",
|
|
139
|
+
"eps"
|
|
140
|
+
];
|
|
141
|
+
var encodeRaster = (image, format) => format === "webp" ? image.webp({ lossless: true }) : image.png();
|
|
31
142
|
function buildSVGString(props) {
|
|
32
|
-
const element =
|
|
143
|
+
const element = createElement(QRCodeSVG, {
|
|
33
144
|
size: props.size ?? 512,
|
|
34
145
|
...props
|
|
35
146
|
});
|
|
36
|
-
return
|
|
147
|
+
return renderToStaticMarkup(element);
|
|
148
|
+
}
|
|
149
|
+
async function loadLogo(src) {
|
|
150
|
+
if (src.startsWith("data:")) {
|
|
151
|
+
const comma = src.indexOf(",");
|
|
152
|
+
const payload = src.slice(comma + 1);
|
|
153
|
+
return src.slice(0, comma).endsWith(";base64") ? Buffer.from(payload, "base64") : Buffer.from(decodeURIComponent(payload));
|
|
154
|
+
}
|
|
155
|
+
if (/^https?:\/\//i.test(src)) {
|
|
156
|
+
const res = await fetch(src);
|
|
157
|
+
if (!res.ok) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Could not download logo (${res.status} ${res.statusText}): ${src}`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return Buffer.from(await res.arrayBuffer());
|
|
163
|
+
}
|
|
164
|
+
return readFile(src);
|
|
165
|
+
}
|
|
166
|
+
async function logoAsEmbeddableDataUrl(src) {
|
|
167
|
+
const bytes = await loadLogo(src);
|
|
168
|
+
if (bytes[0] === 137 && bytes[1] === 80) {
|
|
169
|
+
return `data:image/png;base64,${bytes.toString("base64")}`;
|
|
170
|
+
}
|
|
171
|
+
if (bytes[0] === 255 && bytes[1] === 216) {
|
|
172
|
+
return `data:image/jpeg;base64,${bytes.toString("base64")}`;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const mod = await import('sharp');
|
|
176
|
+
const sharp = mod.default;
|
|
177
|
+
const png = await sharp(bytes).png().toBuffer();
|
|
178
|
+
return `data:image/png;base64,${png.toString("base64")}`;
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async function buildEPSString(props) {
|
|
184
|
+
const src = props.imageSettings?.src;
|
|
185
|
+
let epsProps = props;
|
|
186
|
+
if (src && props.imageSettings) {
|
|
187
|
+
const dataUrl = await logoAsEmbeddableDataUrl(src);
|
|
188
|
+
if (dataUrl) {
|
|
189
|
+
epsProps = {
|
|
190
|
+
...props,
|
|
191
|
+
imageSettings: { ...props.imageSettings, src: dataUrl }
|
|
192
|
+
};
|
|
193
|
+
} else {
|
|
194
|
+
console.warn(
|
|
195
|
+
"The logo could not be converted for EPS (PNG or JPEG work best), so it was left out."
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return await svgToEPS(buildSVGString(epsProps));
|
|
200
|
+
}
|
|
201
|
+
async function findLogoBox(sharp, svgString) {
|
|
202
|
+
const imageTag = svgString.match(/<image\b[^>]*?(?:\/>|><\/image>)/)?.[0];
|
|
203
|
+
if (!imageTag) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
const markerTag = imageTag.replace(/^<image\b/, '<rect class="qrdx-logo-box"').replace(/\shref="[^"]*"/, "").replace(/(?:\/>|><\/image>)$/, ' fill="#000"/>');
|
|
207
|
+
const markerSvg = svgString.replace(imageTag, markerTag).replace(
|
|
208
|
+
/<svg\b[^>]*>/,
|
|
209
|
+
(open) => `${open}<style>*{visibility:hidden}.qrdx-logo-box{visibility:visible}</style>`
|
|
210
|
+
);
|
|
211
|
+
const { info } = await sharp(Buffer.from(markerSvg, "utf-8")).png().trim().toBuffer({ resolveWithObject: true });
|
|
212
|
+
return {
|
|
213
|
+
left: -(info.trimOffsetLeft ?? 0),
|
|
214
|
+
top: -(info.trimOffsetTop ?? 0),
|
|
215
|
+
width: info.width,
|
|
216
|
+
height: info.height
|
|
217
|
+
};
|
|
37
218
|
}
|
|
38
219
|
async function saveOutput(options) {
|
|
39
220
|
const { outputPath, format, ...qrProps } = options;
|
|
@@ -41,93 +222,259 @@ async function saveOutput(options) {
|
|
|
41
222
|
if (format === "svg") {
|
|
42
223
|
const svgContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
43
224
|
${svgString}`;
|
|
44
|
-
|
|
225
|
+
writeFileSync(outputPath, svgContent, "utf-8");
|
|
45
226
|
return;
|
|
46
227
|
}
|
|
47
|
-
|
|
228
|
+
if (format === "eps") {
|
|
229
|
+
writeFileSync(outputPath, await buildEPSString(qrProps), "utf-8");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
let sharp;
|
|
233
|
+
try {
|
|
234
|
+
const mod = await import('sharp');
|
|
235
|
+
sharp = mod.default;
|
|
236
|
+
} catch {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`${format === "webp" ? "WebP" : "PNG"} output needs the sharp package (native binaries). It is optional and may not have installed; use SVG or reinstall: https://sharp.pixelplumbing.com/install`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
48
241
|
const svgBuffer = Buffer.from(svgString, "utf-8");
|
|
49
242
|
const size = qrProps.size ?? 512;
|
|
50
|
-
|
|
243
|
+
const { width, height } = getFrameDimensions(size, qrProps.frame);
|
|
244
|
+
const qrImage = sharp(svgBuffer).resize(
|
|
245
|
+
Math.round(width),
|
|
246
|
+
Math.round(height)
|
|
247
|
+
);
|
|
248
|
+
const logoSrc = qrProps.imageSettings?.src;
|
|
249
|
+
const box = logoSrc ? await findLogoBox(sharp, svgString) : null;
|
|
250
|
+
if (!(logoSrc && box)) {
|
|
251
|
+
await encodeRaster(qrImage, format).toFile(outputPath);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const logo = await sharp(await loadLogo(logoSrc)).resize(box.width, box.height, { fit: "fill" }).png().toBuffer();
|
|
255
|
+
const composited = sharp(await qrImage.png().toBuffer()).composite([
|
|
256
|
+
{ input: logo, left: box.left, top: box.top }
|
|
257
|
+
]);
|
|
258
|
+
await encodeRaster(composited, format).toFile(outputPath);
|
|
51
259
|
}
|
|
52
|
-
|
|
53
|
-
// src/commands/generate.ts
|
|
54
260
|
var RESET = "\x1B[0m";
|
|
55
261
|
var TEXT = "\x1B[38;5;145m";
|
|
56
262
|
var GREEN = "\x1B[38;5;114m";
|
|
57
263
|
var DIM = "\x1B[38;5;102m";
|
|
264
|
+
var HEX_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
265
|
+
var MAX_PNG_SIZE = 1e4;
|
|
58
266
|
function parseColorFlag(value) {
|
|
59
267
|
const trimmed = value.trim();
|
|
60
|
-
if (trimmed.startsWith("{")) {
|
|
268
|
+
if (!trimmed.startsWith("{")) {
|
|
269
|
+
return trimmed;
|
|
270
|
+
}
|
|
271
|
+
let parsed;
|
|
272
|
+
try {
|
|
273
|
+
parsed = JSON.parse(trimmed);
|
|
274
|
+
} catch {
|
|
275
|
+
throw new Error(`Invalid gradient JSON: ${trimmed}`);
|
|
276
|
+
}
|
|
277
|
+
const result = colorConfigSchema.safeParse(parsed);
|
|
278
|
+
if (!result.success) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
`Invalid gradient: ${trimmed}
|
|
281
|
+
Expected {"type":"linear"|"radial"|"angular","stops":[{"color":"#hex","offset":0-100}, ...]} with at least 2 stops.`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return result.data;
|
|
285
|
+
}
|
|
286
|
+
function assertOneOf(flag, value, allowed) {
|
|
287
|
+
if (value !== void 0 && !allowed.includes(value)) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
`Invalid ${flag} "${value}". Expected one of: ${allowed.join(", ")}`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function parseIntegerFlag(flag, value, min) {
|
|
294
|
+
const parsed = Number(value);
|
|
295
|
+
if (!(value && Number.isInteger(parsed) && parsed >= min)) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`Invalid ${flag} "${value ?? ""}". Expected a whole number \u2265 ${min}.`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
return parsed;
|
|
301
|
+
}
|
|
302
|
+
function assertHexColor(flag, value) {
|
|
303
|
+
if (value !== void 0 && !HEX_COLOR.test(value)) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`Invalid ${flag} "${value}". Expected a hex color like #000000.`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function requireValue(flag, value) {
|
|
310
|
+
if (value === void 0 || /^--?[a-z]/i.test(value)) {
|
|
311
|
+
throw new Error(`${flag} needs a value. Run qrdx --help to see options.`);
|
|
312
|
+
}
|
|
313
|
+
return value;
|
|
314
|
+
}
|
|
315
|
+
function frameFromFlags(flags) {
|
|
316
|
+
const style = flags.frame ?? (flags.frameText !== void 0 || flags.frameColor !== void 0 || flags.frameTextColor !== void 0 ? "banner-bottom" : void 0);
|
|
317
|
+
if (!style || style === "none") {
|
|
318
|
+
return void 0;
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
style,
|
|
322
|
+
...flags.frameText !== void 0 && { text: flags.frameText },
|
|
323
|
+
...flags.frameColor && { color: flags.frameColor },
|
|
324
|
+
...flags.frameTextColor && { textColor: flags.frameTextColor }
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function validateFlags(flags) {
|
|
328
|
+
assertOneOf("--level", flags.level, errorLevelSchema.options);
|
|
329
|
+
assertOneOf("--body", flags.body, BODY_PATTERN);
|
|
330
|
+
assertOneOf("--eye", flags.eye, CORNER_EYE_PATTERNS);
|
|
331
|
+
assertOneOf("--dot", flags.dot, CORNER_EYE_DOT_PATTERNS);
|
|
332
|
+
assertOneOf(
|
|
333
|
+
"--template",
|
|
334
|
+
flags.template,
|
|
335
|
+
getTemplateIds().filter((id) => id !== "default")
|
|
336
|
+
);
|
|
337
|
+
assertOneOf("--frame", flags.frame, FRAME_STYLES);
|
|
338
|
+
if (flags.frameText !== void 0 && flags.frameText.length > FRAME_TEXT_MAX_LENGTH) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`Invalid --frame-text "${flags.frameText}". Use ${FRAME_TEXT_MAX_LENGTH} characters or fewer.`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
assertHexColor("--frame-color", flags.frameColor);
|
|
344
|
+
assertHexColor("--frame-text-color", flags.frameTextColor);
|
|
345
|
+
assertOneOf("--format", flags.format, OUTPUT_FORMATS);
|
|
346
|
+
if (flags.output !== void 0) {
|
|
347
|
+
const ext = extname(flags.output).toLowerCase();
|
|
348
|
+
if (!["", ".svg", ".png", ".webp", ".eps"].includes(ext)) {
|
|
349
|
+
throw new Error(
|
|
350
|
+
`Unsupported output format "${ext}". Use a .svg, .png, .webp or .eps file name.`
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
if (ext && flags.format && ext !== `.${flags.format}`) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`--format ${flags.format} does not match the output file "${flags.output}". Use a .${flags.format} file name or drop --format.`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const raster = [
|
|
360
|
+
extname(flags.output ?? "").toLowerCase().slice(1),
|
|
361
|
+
flags.format
|
|
362
|
+
].find((format) => format === "png" || format === "webp");
|
|
363
|
+
if (raster && flags.size !== void 0 && flags.size > MAX_PNG_SIZE) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`Invalid --size "${flags.size}". ${raster === "webp" ? "WebP" : "PNG"} output is at most ${MAX_PNG_SIZE} px; use .svg or .eps for larger prints.`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
var isCI = process.env.CI === "true";
|
|
370
|
+
var canUseClack = process.stdin.isTTY === true && process.stdout.isTTY === true && !isCI;
|
|
371
|
+
function readLineFromTTY(question) {
|
|
372
|
+
if (process.platform === "win32") {
|
|
373
|
+
return Promise.resolve(void 0);
|
|
374
|
+
}
|
|
375
|
+
return new Promise((resolve2) => {
|
|
376
|
+
let stream;
|
|
61
377
|
try {
|
|
62
|
-
|
|
378
|
+
stream = createReadStream("/dev/tty", { flags: "r" });
|
|
63
379
|
} catch {
|
|
380
|
+
resolve2(void 0);
|
|
381
|
+
return;
|
|
64
382
|
}
|
|
65
|
-
|
|
66
|
-
|
|
383
|
+
const rl = readline.createInterface({
|
|
384
|
+
input: stream,
|
|
385
|
+
output: process.stdout
|
|
386
|
+
});
|
|
387
|
+
const finish = (answer) => {
|
|
388
|
+
rl.close();
|
|
389
|
+
stream.destroy();
|
|
390
|
+
resolve2(answer);
|
|
391
|
+
};
|
|
392
|
+
stream.once("error", () => finish(void 0));
|
|
393
|
+
rl.question(question, (answer) => finish(answer));
|
|
394
|
+
});
|
|
67
395
|
}
|
|
68
|
-
var IS_TTY = Boolean(process.stdin.isTTY);
|
|
69
396
|
function parseFlags(args) {
|
|
70
397
|
const flags = {};
|
|
71
398
|
let i = 0;
|
|
72
399
|
while (i < args.length) {
|
|
73
400
|
const arg = args[i];
|
|
74
401
|
if (!arg.startsWith("-")) {
|
|
75
|
-
if (
|
|
76
|
-
|
|
402
|
+
if (flags.value !== void 0) {
|
|
403
|
+
throw new Error(
|
|
404
|
+
`Unexpected argument "${arg}". Quote text with spaces: qrdx generate "${flags.value} ${arg}"`
|
|
405
|
+
);
|
|
77
406
|
}
|
|
407
|
+
flags.value = arg;
|
|
78
408
|
i++;
|
|
79
409
|
continue;
|
|
80
410
|
}
|
|
81
411
|
const next = args[i + 1];
|
|
82
412
|
switch (arg) {
|
|
413
|
+
case "-h":
|
|
414
|
+
case "--help":
|
|
415
|
+
flags.help = true;
|
|
416
|
+
i++;
|
|
417
|
+
break;
|
|
418
|
+
case "-v":
|
|
419
|
+
case "--version":
|
|
420
|
+
flags.version = true;
|
|
421
|
+
i++;
|
|
422
|
+
break;
|
|
83
423
|
case "-o":
|
|
84
424
|
case "--output":
|
|
425
|
+
if (!next || /^--?[a-z]/i.test(next)) {
|
|
426
|
+
throw new Error(`${arg} needs a file path, e.g. ${arg} qr.svg`);
|
|
427
|
+
}
|
|
85
428
|
flags.output = next;
|
|
86
429
|
i += 2;
|
|
87
430
|
break;
|
|
431
|
+
case "--format":
|
|
432
|
+
flags.format = requireValue(arg, next).toLowerCase();
|
|
433
|
+
i += 2;
|
|
434
|
+
break;
|
|
88
435
|
case "-l":
|
|
89
436
|
case "--level":
|
|
90
|
-
flags.level = next;
|
|
437
|
+
flags.level = requireValue(arg, next).toUpperCase();
|
|
91
438
|
i += 2;
|
|
92
439
|
break;
|
|
93
440
|
case "--body":
|
|
94
|
-
flags.body = next;
|
|
441
|
+
flags.body = requireValue(arg, next);
|
|
95
442
|
i += 2;
|
|
96
443
|
break;
|
|
97
444
|
case "--eye":
|
|
98
|
-
flags.eye = next;
|
|
445
|
+
flags.eye = requireValue(arg, next);
|
|
99
446
|
i += 2;
|
|
100
447
|
break;
|
|
101
448
|
case "--dot":
|
|
102
|
-
flags.dot = next;
|
|
449
|
+
flags.dot = requireValue(arg, next);
|
|
103
450
|
i += 2;
|
|
104
451
|
break;
|
|
105
452
|
case "--fg":
|
|
106
|
-
flags.fg =
|
|
453
|
+
flags.fg = parseColorFlag(requireValue(arg, next));
|
|
107
454
|
i += 2;
|
|
108
455
|
break;
|
|
109
456
|
case "--bg":
|
|
110
|
-
flags.bg =
|
|
457
|
+
flags.bg = parseColorFlag(requireValue(arg, next));
|
|
111
458
|
i += 2;
|
|
112
459
|
break;
|
|
113
460
|
case "--eye-color":
|
|
114
|
-
flags.eyeColor =
|
|
461
|
+
flags.eyeColor = parseColorFlag(requireValue(arg, next));
|
|
115
462
|
i += 2;
|
|
116
463
|
break;
|
|
117
464
|
case "--dot-color":
|
|
118
|
-
flags.dotColor =
|
|
465
|
+
flags.dotColor = parseColorFlag(requireValue(arg, next));
|
|
119
466
|
i += 2;
|
|
120
467
|
break;
|
|
121
468
|
case "--logo":
|
|
122
|
-
flags.logo = next;
|
|
469
|
+
flags.logo = requireValue(arg, next);
|
|
123
470
|
i += 2;
|
|
124
471
|
break;
|
|
125
472
|
case "--margin":
|
|
126
|
-
flags.margin =
|
|
473
|
+
flags.margin = parseIntegerFlag(arg, next, 0);
|
|
127
474
|
i += 2;
|
|
128
475
|
break;
|
|
129
476
|
case "--size":
|
|
130
|
-
flags.size =
|
|
477
|
+
flags.size = parseIntegerFlag(arg, next, 1);
|
|
131
478
|
i += 2;
|
|
132
479
|
break;
|
|
133
480
|
case "--no-logo":
|
|
@@ -135,77 +482,193 @@ function parseFlags(args) {
|
|
|
135
482
|
i++;
|
|
136
483
|
break;
|
|
137
484
|
case "--template":
|
|
138
|
-
flags.template = next;
|
|
485
|
+
flags.template = requireValue(arg, next);
|
|
486
|
+
i += 2;
|
|
487
|
+
break;
|
|
488
|
+
case "--frame":
|
|
489
|
+
flags.frame = requireValue(arg, next);
|
|
490
|
+
i += 2;
|
|
491
|
+
break;
|
|
492
|
+
case "--frame-text":
|
|
493
|
+
flags.frameText = requireValue(arg, next);
|
|
494
|
+
i += 2;
|
|
495
|
+
break;
|
|
496
|
+
case "--frame-color":
|
|
497
|
+
flags.frameColor = requireValue(arg, next);
|
|
498
|
+
i += 2;
|
|
499
|
+
break;
|
|
500
|
+
case "--frame-text-color":
|
|
501
|
+
flags.frameTextColor = requireValue(arg, next);
|
|
139
502
|
i += 2;
|
|
140
503
|
break;
|
|
141
504
|
default:
|
|
142
|
-
|
|
505
|
+
throw new Error(
|
|
506
|
+
`Unknown option "${arg}". Run qrdx --help to see all options.`
|
|
507
|
+
);
|
|
143
508
|
}
|
|
144
509
|
}
|
|
510
|
+
if (!(flags.help || flags.version)) validateFlags(flags);
|
|
145
511
|
return flags;
|
|
146
512
|
}
|
|
513
|
+
function buildQrProps(value, settings) {
|
|
514
|
+
const { size, logo, template, frame, eyeColor, dotColor } = settings;
|
|
515
|
+
return {
|
|
516
|
+
value,
|
|
517
|
+
level: settings.level,
|
|
518
|
+
bodyPattern: settings.body,
|
|
519
|
+
cornerEyePattern: settings.eye,
|
|
520
|
+
cornerEyeDotPattern: settings.dot,
|
|
521
|
+
fgColor: settings.fg,
|
|
522
|
+
bgColor: settings.bg,
|
|
523
|
+
...eyeColor ? { eyeColor } : {},
|
|
524
|
+
...dotColor ? { dotColor } : {},
|
|
525
|
+
margin: settings.margin,
|
|
526
|
+
size,
|
|
527
|
+
...template ? { templateId: template } : {},
|
|
528
|
+
...frame ? { frame } : {},
|
|
529
|
+
...logo ? {
|
|
530
|
+
imageSettings: {
|
|
531
|
+
src: logo,
|
|
532
|
+
height: Math.round(size * 0.2),
|
|
533
|
+
width: Math.round(size * 0.2),
|
|
534
|
+
excavate: true
|
|
535
|
+
}
|
|
536
|
+
} : {}
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function styleFromArgs(args) {
|
|
540
|
+
const flags = parseFlags(args);
|
|
541
|
+
return {
|
|
542
|
+
flags,
|
|
543
|
+
settings: {
|
|
544
|
+
level: flags.level ?? "Q",
|
|
545
|
+
body: flags.body ?? "square",
|
|
546
|
+
eye: flags.eye ?? "square",
|
|
547
|
+
dot: flags.dot ?? "square",
|
|
548
|
+
fg: flags.fg ?? "#000000",
|
|
549
|
+
bg: flags.bg ?? "#ffffff",
|
|
550
|
+
eyeColor: flags.eyeColor,
|
|
551
|
+
dotColor: flags.dotColor,
|
|
552
|
+
margin: flags.margin ?? DEFAULT_MARGIN,
|
|
553
|
+
size: flags.size ?? 512,
|
|
554
|
+
template: flags.template,
|
|
555
|
+
frame: frameFromFlags(flags),
|
|
556
|
+
logo: flags.logo
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
}
|
|
147
560
|
function isCancel2(value) {
|
|
148
|
-
return
|
|
561
|
+
return p.isCancel(value);
|
|
149
562
|
}
|
|
150
563
|
function handleCancel() {
|
|
151
|
-
|
|
564
|
+
p.cancel("Cancelled.");
|
|
565
|
+
console.error(
|
|
566
|
+
`${DIM}Tip:${RESET} ${TEXT}qrdx generate "https://\u2026" -o qr.svg${RESET} \u2014 local dev: ${TEXT}pnpm qrdx -- generate \u2026${RESET}`
|
|
567
|
+
);
|
|
152
568
|
process.exit(0);
|
|
153
569
|
}
|
|
154
|
-
function inferFormat(outputPath) {
|
|
155
|
-
const ext =
|
|
570
|
+
function inferFormat(outputPath, format) {
|
|
571
|
+
const ext = extname(outputPath).toLowerCase();
|
|
156
572
|
if (ext === ".png") {
|
|
157
573
|
return "png";
|
|
158
574
|
}
|
|
159
|
-
|
|
575
|
+
if (ext === ".webp") {
|
|
576
|
+
return "webp";
|
|
577
|
+
}
|
|
578
|
+
if (ext === ".eps") {
|
|
579
|
+
return "eps";
|
|
580
|
+
}
|
|
581
|
+
return ext === ".svg" ? "svg" : format ?? "svg";
|
|
160
582
|
}
|
|
161
|
-
async function runGenerate(args) {
|
|
583
|
+
async function runGenerate(args, meta) {
|
|
162
584
|
const flags = parseFlags(args);
|
|
163
|
-
|
|
585
|
+
if (meta && flags.help) return meta.help();
|
|
586
|
+
if (meta && flags.version) return meta.version();
|
|
587
|
+
meta?.start?.();
|
|
588
|
+
const nonInteractive = !canUseClack;
|
|
164
589
|
const hasAdvancedFlags = Boolean(
|
|
165
|
-
flags.body || flags.eye || flags.dot || flags.fg || flags.bg || flags.eyeColor || flags.dotColor || flags.logo || flags.level || flags.margin !== void 0 || flags.template
|
|
590
|
+
flags.body || flags.eye || flags.dot || flags.fg || flags.bg || flags.eyeColor || flags.dotColor || flags.logo || flags.level || flags.margin !== void 0 || flags.template || frameFromFlags(flags)
|
|
166
591
|
);
|
|
167
|
-
|
|
592
|
+
p.intro(`${TEXT}qrdx${RESET} ${DIM}\u2014 QR Code Generator${RESET}`);
|
|
593
|
+
if (canUseClack) {
|
|
594
|
+
process.stdin.resume();
|
|
595
|
+
}
|
|
168
596
|
let value = flags.value;
|
|
169
597
|
if (!value) {
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return "Please enter a value to encode.";
|
|
598
|
+
if (canUseClack) {
|
|
599
|
+
const promptStart = performance.now();
|
|
600
|
+
const res = await p.text({
|
|
601
|
+
message: "URL or text to encode",
|
|
602
|
+
placeholder: "https://qrdx.dev",
|
|
603
|
+
validate(v) {
|
|
604
|
+
if (!v?.trim()) {
|
|
605
|
+
return "Please enter a value to encode.";
|
|
606
|
+
}
|
|
180
607
|
}
|
|
608
|
+
});
|
|
609
|
+
if (isCancel2(res)) {
|
|
610
|
+
const instant = performance.now() - promptStart < 120 && process.platform !== "win32" && process.stdout.isTTY && !isCI;
|
|
611
|
+
if (instant) {
|
|
612
|
+
const line = await readLineFromTTY(
|
|
613
|
+
`${TEXT}URL or text to encode${RESET} ${DIM}(e.g. https://qrdx.dev)${RESET}: `
|
|
614
|
+
);
|
|
615
|
+
if (line?.trim()) {
|
|
616
|
+
value = line.trim();
|
|
617
|
+
} else {
|
|
618
|
+
handleCancel();
|
|
619
|
+
}
|
|
620
|
+
} else {
|
|
621
|
+
handleCancel();
|
|
622
|
+
}
|
|
623
|
+
} else {
|
|
624
|
+
value = res;
|
|
181
625
|
}
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
|
|
626
|
+
} else if (process.stdout.isTTY && !isCI) {
|
|
627
|
+
const line = await readLineFromTTY(
|
|
628
|
+
`${TEXT}URL or text to encode${RESET} ${DIM}(e.g. https://qrdx.dev)${RESET}: `
|
|
629
|
+
);
|
|
630
|
+
if (line?.trim()) {
|
|
631
|
+
value = line.trim();
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (!value) {
|
|
635
|
+
console.error(
|
|
636
|
+
'No data to encode. Example: npx @qrdx/cli generate "https://example.com" -o qr.svg'
|
|
637
|
+
);
|
|
638
|
+
process.exit(1);
|
|
185
639
|
}
|
|
186
|
-
value = res;
|
|
187
640
|
}
|
|
188
641
|
let outputPath = flags.output;
|
|
189
642
|
const size = flags.size ?? 512;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
643
|
+
const defaultOutput = `qr.${flags.format ?? "svg"}`;
|
|
644
|
+
if (!outputPath) {
|
|
645
|
+
if (canUseClack) {
|
|
646
|
+
const pathRes = await p.text({
|
|
647
|
+
message: "Output file (.svg, .png, .webp or .eps)",
|
|
648
|
+
placeholder: defaultOutput,
|
|
649
|
+
defaultValue: defaultOutput,
|
|
650
|
+
validate(v) {
|
|
651
|
+
if (!v?.trim()) {
|
|
652
|
+
return "Please enter an output path.";
|
|
653
|
+
}
|
|
198
654
|
}
|
|
655
|
+
});
|
|
656
|
+
if (isCancel2(pathRes)) {
|
|
657
|
+
handleCancel();
|
|
199
658
|
}
|
|
200
|
-
|
|
201
|
-
if (
|
|
202
|
-
|
|
659
|
+
outputPath = pathRes || defaultOutput;
|
|
660
|
+
} else if (process.stdout.isTTY && !isCI) {
|
|
661
|
+
const pathAns = await readLineFromTTY(
|
|
662
|
+
`Output file ${DIM}(.svg, .png, .webp or .eps, Enter = ${defaultOutput})${RESET}: `
|
|
663
|
+
);
|
|
664
|
+
outputPath = pathAns?.trim() || defaultOutput;
|
|
665
|
+
} else {
|
|
666
|
+
outputPath = defaultOutput;
|
|
203
667
|
}
|
|
204
|
-
outputPath = pathRes || "qr.svg";
|
|
205
668
|
}
|
|
206
669
|
let wantAdvanced = hasAdvancedFlags;
|
|
207
670
|
if (!(hasAdvancedFlags || nonInteractive)) {
|
|
208
|
-
const res = await
|
|
671
|
+
const res = await p.confirm({
|
|
209
672
|
message: "Advanced customizations?",
|
|
210
673
|
initialValue: false
|
|
211
674
|
});
|
|
@@ -223,11 +686,12 @@ async function runGenerate(args) {
|
|
|
223
686
|
let eyeColor = flags.eyeColor;
|
|
224
687
|
let dotColor = flags.dotColor;
|
|
225
688
|
let logo = flags.logo;
|
|
226
|
-
let margin = flags.margin ??
|
|
689
|
+
let margin = flags.margin ?? DEFAULT_MARGIN;
|
|
227
690
|
let template = flags.template;
|
|
691
|
+
let frame = frameFromFlags(flags);
|
|
228
692
|
if (wantAdvanced && !nonInteractive) {
|
|
229
693
|
if (!flags.level) {
|
|
230
|
-
const res = await
|
|
694
|
+
const res = await p.select({
|
|
231
695
|
message: "Error correction level",
|
|
232
696
|
options: [
|
|
233
697
|
{
|
|
@@ -247,9 +711,9 @@ async function runGenerate(args) {
|
|
|
247
711
|
level = res;
|
|
248
712
|
}
|
|
249
713
|
if (!flags.body) {
|
|
250
|
-
const res = await
|
|
714
|
+
const res = await p.select({
|
|
251
715
|
message: "Body dot pattern",
|
|
252
|
-
options:
|
|
716
|
+
options: BODY_PATTERN.map((b) => ({ value: b, label: b })),
|
|
253
717
|
initialValue: "square"
|
|
254
718
|
});
|
|
255
719
|
if (isCancel2(res)) {
|
|
@@ -258,9 +722,9 @@ async function runGenerate(args) {
|
|
|
258
722
|
body = res;
|
|
259
723
|
}
|
|
260
724
|
if (!flags.eye) {
|
|
261
|
-
const res = await
|
|
725
|
+
const res = await p.select({
|
|
262
726
|
message: "Corner eye pattern",
|
|
263
|
-
options:
|
|
727
|
+
options: CORNER_EYE_PATTERNS.map((e) => ({ value: e, label: e })),
|
|
264
728
|
initialValue: "square"
|
|
265
729
|
});
|
|
266
730
|
if (isCancel2(res)) {
|
|
@@ -269,9 +733,9 @@ async function runGenerate(args) {
|
|
|
269
733
|
eye = res;
|
|
270
734
|
}
|
|
271
735
|
if (!flags.dot) {
|
|
272
|
-
const res = await
|
|
736
|
+
const res = await p.select({
|
|
273
737
|
message: "Corner dot pattern",
|
|
274
|
-
options:
|
|
738
|
+
options: CORNER_EYE_DOT_PATTERNS.map((d) => ({ value: d, label: d })),
|
|
275
739
|
initialValue: "square"
|
|
276
740
|
});
|
|
277
741
|
if (isCancel2(res)) {
|
|
@@ -280,13 +744,13 @@ async function runGenerate(args) {
|
|
|
280
744
|
dot = res;
|
|
281
745
|
}
|
|
282
746
|
if (!flags.fg) {
|
|
283
|
-
const res = await
|
|
747
|
+
const res = await p.text({
|
|
284
748
|
message: `Foreground color ${DIM}(hex or JSON gradient)${RESET}`,
|
|
285
749
|
placeholder: "#000000",
|
|
286
750
|
defaultValue: "#000000",
|
|
287
751
|
validate(v) {
|
|
288
752
|
if (!v) {
|
|
289
|
-
return
|
|
753
|
+
return;
|
|
290
754
|
}
|
|
291
755
|
if (v.trim().startsWith("{")) {
|
|
292
756
|
try {
|
|
@@ -294,7 +758,7 @@ async function runGenerate(args) {
|
|
|
294
758
|
} catch {
|
|
295
759
|
return "Enter a valid hex color or JSON gradient object";
|
|
296
760
|
}
|
|
297
|
-
return
|
|
761
|
+
return;
|
|
298
762
|
}
|
|
299
763
|
if (!/^#[0-9a-fA-F]{3,8}$/.test(v)) {
|
|
300
764
|
return "Enter a valid hex color (e.g. #000000) or JSON gradient";
|
|
@@ -307,13 +771,13 @@ async function runGenerate(args) {
|
|
|
307
771
|
fg = parseColorFlag(res || "#000000");
|
|
308
772
|
}
|
|
309
773
|
if (!flags.bg) {
|
|
310
|
-
const res = await
|
|
774
|
+
const res = await p.text({
|
|
311
775
|
message: `Background color ${DIM}(hex or JSON gradient)${RESET}`,
|
|
312
776
|
placeholder: "#ffffff",
|
|
313
777
|
defaultValue: "#ffffff",
|
|
314
778
|
validate(v) {
|
|
315
779
|
if (!v) {
|
|
316
|
-
return
|
|
780
|
+
return;
|
|
317
781
|
}
|
|
318
782
|
if (v.trim().startsWith("{")) {
|
|
319
783
|
try {
|
|
@@ -321,7 +785,7 @@ async function runGenerate(args) {
|
|
|
321
785
|
} catch {
|
|
322
786
|
return "Enter a valid hex color or JSON gradient object";
|
|
323
787
|
}
|
|
324
|
-
return
|
|
788
|
+
return;
|
|
325
789
|
}
|
|
326
790
|
if (!/^#[0-9a-fA-F]{3,8}$/.test(v)) {
|
|
327
791
|
return "Enter a valid hex color (e.g. #ffffff) or JSON gradient";
|
|
@@ -334,12 +798,12 @@ async function runGenerate(args) {
|
|
|
334
798
|
bg = parseColorFlag(res || "#ffffff");
|
|
335
799
|
}
|
|
336
800
|
if (eyeColor === void 0) {
|
|
337
|
-
const res = await
|
|
801
|
+
const res = await p.text({
|
|
338
802
|
message: `Corner eye color ${DIM}(leave blank to match foreground)${RESET}`,
|
|
339
803
|
placeholder: "leave blank",
|
|
340
804
|
validate(v) {
|
|
341
805
|
if (!v) {
|
|
342
|
-
return
|
|
806
|
+
return;
|
|
343
807
|
}
|
|
344
808
|
if (v.trim().startsWith("{")) {
|
|
345
809
|
try {
|
|
@@ -347,7 +811,7 @@ async function runGenerate(args) {
|
|
|
347
811
|
} catch {
|
|
348
812
|
return "Enter a valid hex color or JSON gradient object";
|
|
349
813
|
}
|
|
350
|
-
return
|
|
814
|
+
return;
|
|
351
815
|
}
|
|
352
816
|
if (!/^#[0-9a-fA-F]{3,8}$/.test(v)) {
|
|
353
817
|
return "Enter a valid hex color or leave blank";
|
|
@@ -361,12 +825,12 @@ async function runGenerate(args) {
|
|
|
361
825
|
eyeColor = eyeVal ? parseColorFlag(eyeVal) : void 0;
|
|
362
826
|
}
|
|
363
827
|
if (dotColor === void 0) {
|
|
364
|
-
const res = await
|
|
828
|
+
const res = await p.text({
|
|
365
829
|
message: `Corner dot color ${DIM}(leave blank to match foreground)${RESET}`,
|
|
366
830
|
placeholder: "leave blank",
|
|
367
831
|
validate(v) {
|
|
368
832
|
if (!v) {
|
|
369
|
-
return
|
|
833
|
+
return;
|
|
370
834
|
}
|
|
371
835
|
if (v.trim().startsWith("{")) {
|
|
372
836
|
try {
|
|
@@ -374,7 +838,7 @@ async function runGenerate(args) {
|
|
|
374
838
|
} catch {
|
|
375
839
|
return "Enter a valid hex color or JSON gradient object";
|
|
376
840
|
}
|
|
377
|
-
return
|
|
841
|
+
return;
|
|
378
842
|
}
|
|
379
843
|
if (!/^#[0-9a-fA-F]{3,8}$/.test(v)) {
|
|
380
844
|
return "Enter a valid hex color or leave blank";
|
|
@@ -388,8 +852,8 @@ async function runGenerate(args) {
|
|
|
388
852
|
dotColor = dotVal ? parseColorFlag(dotVal) : void 0;
|
|
389
853
|
}
|
|
390
854
|
if (!flags.template) {
|
|
391
|
-
const templateIds =
|
|
392
|
-
const res = await
|
|
855
|
+
const templateIds = getTemplateIds();
|
|
856
|
+
const res = await p.select({
|
|
393
857
|
message: `Template ${DIM}(decorative wrapper around the QR code)${RESET}`,
|
|
394
858
|
options: [
|
|
395
859
|
{ value: "", label: "none" },
|
|
@@ -402,8 +866,37 @@ async function runGenerate(args) {
|
|
|
402
866
|
}
|
|
403
867
|
template = res || void 0;
|
|
404
868
|
}
|
|
869
|
+
if (flags.frame === void 0 && !frame) {
|
|
870
|
+
const res = await p.select({
|
|
871
|
+
message: `Frame ${DIM}(call-to-action label around the code)${RESET}`,
|
|
872
|
+
options: FRAME_STYLES.map((style) => ({ value: style, label: style })),
|
|
873
|
+
initialValue: "none"
|
|
874
|
+
});
|
|
875
|
+
if (isCancel2(res)) {
|
|
876
|
+
handleCancel();
|
|
877
|
+
}
|
|
878
|
+
if (res !== "none") {
|
|
879
|
+
const text2 = await p.text({
|
|
880
|
+
message: `Frame text ${DIM}(up to ${FRAME_TEXT_MAX_LENGTH} characters)${RESET}`,
|
|
881
|
+
placeholder: DEFAULT_FRAME_TEXT,
|
|
882
|
+
defaultValue: DEFAULT_FRAME_TEXT,
|
|
883
|
+
validate(v) {
|
|
884
|
+
if (v && v.length > FRAME_TEXT_MAX_LENGTH) {
|
|
885
|
+
return `Use ${FRAME_TEXT_MAX_LENGTH} characters or fewer`;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
if (isCancel2(text2)) {
|
|
890
|
+
handleCancel();
|
|
891
|
+
}
|
|
892
|
+
frame = {
|
|
893
|
+
style: res,
|
|
894
|
+
text: text2 || DEFAULT_FRAME_TEXT
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
}
|
|
405
898
|
if (logo === void 0 && !flags.noLogo) {
|
|
406
|
-
const addLogo = await
|
|
899
|
+
const addLogo = await p.confirm({
|
|
407
900
|
message: "Add a logo to the center?",
|
|
408
901
|
initialValue: false
|
|
409
902
|
});
|
|
@@ -411,16 +904,14 @@ async function runGenerate(args) {
|
|
|
411
904
|
handleCancel();
|
|
412
905
|
}
|
|
413
906
|
if (addLogo) {
|
|
414
|
-
const res = await
|
|
907
|
+
const res = await p.text({
|
|
415
908
|
message: "Logo URL",
|
|
416
909
|
placeholder: "https://qrdx.dev/logo.png",
|
|
417
910
|
validate(v) {
|
|
418
|
-
if (!v
|
|
911
|
+
if (!v?.trim()) {
|
|
419
912
|
return "Please enter a URL for the logo.";
|
|
420
913
|
}
|
|
421
|
-
|
|
422
|
-
new URL(v);
|
|
423
|
-
} catch {
|
|
914
|
+
if (!URL.canParse(v)) {
|
|
424
915
|
return "Please enter a valid URL.";
|
|
425
916
|
}
|
|
426
917
|
}
|
|
@@ -432,10 +923,10 @@ async function runGenerate(args) {
|
|
|
432
923
|
}
|
|
433
924
|
}
|
|
434
925
|
if (flags.margin === void 0) {
|
|
435
|
-
const res = await
|
|
926
|
+
const res = await p.text({
|
|
436
927
|
message: "Quiet zone margin (modules)",
|
|
437
|
-
placeholder: String(
|
|
438
|
-
defaultValue: String(
|
|
928
|
+
placeholder: String(DEFAULT_MARGIN),
|
|
929
|
+
defaultValue: String(DEFAULT_MARGIN),
|
|
439
930
|
validate(v) {
|
|
440
931
|
if (v && Number.isNaN(Number.parseInt(v, 10))) {
|
|
441
932
|
return "Enter a number";
|
|
@@ -448,57 +939,247 @@ async function runGenerate(args) {
|
|
|
448
939
|
if (isCancel2(res)) {
|
|
449
940
|
handleCancel();
|
|
450
941
|
}
|
|
451
|
-
margin = Number.parseInt(res || String(
|
|
942
|
+
margin = Number.parseInt(res || String(DEFAULT_MARGIN), 10);
|
|
452
943
|
}
|
|
453
944
|
}
|
|
454
|
-
const spinner2 =
|
|
945
|
+
const spinner2 = p.spinner();
|
|
455
946
|
spinner2.start("Generating QR code...");
|
|
456
947
|
try {
|
|
457
|
-
const qrProps = {
|
|
458
|
-
value,
|
|
948
|
+
const qrProps = buildQrProps(value, {
|
|
459
949
|
level,
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
950
|
+
body,
|
|
951
|
+
eye,
|
|
952
|
+
dot,
|
|
953
|
+
fg,
|
|
954
|
+
bg,
|
|
955
|
+
eyeColor,
|
|
956
|
+
dotColor,
|
|
467
957
|
margin,
|
|
468
958
|
size,
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
height: Math.round(size * 0.2),
|
|
474
|
-
width: Math.round(size * 0.2),
|
|
475
|
-
excavate: true
|
|
476
|
-
}
|
|
477
|
-
} : {}
|
|
478
|
-
};
|
|
959
|
+
template,
|
|
960
|
+
frame,
|
|
961
|
+
logo
|
|
962
|
+
});
|
|
479
963
|
if (outputPath) {
|
|
480
|
-
const format = inferFormat(outputPath);
|
|
964
|
+
const format = inferFormat(outputPath, flags.format);
|
|
481
965
|
await saveOutput({ ...qrProps, outputPath, format });
|
|
966
|
+
const dimensions = getFrameDimensions(size, frame);
|
|
482
967
|
spinner2.stop(
|
|
483
|
-
`${GREEN}\u2714${RESET} Saved to ${TEXT}${outputPath}${RESET} ${DIM}(${
|
|
968
|
+
`${GREEN}\u2714${RESET} Saved to ${TEXT}${outputPath}${RESET} ${DIM}(${Math.round(dimensions.width)}\xD7${Math.round(dimensions.height)})${RESET}`
|
|
484
969
|
);
|
|
485
970
|
} else {
|
|
486
971
|
spinner2.stop("QR code ready");
|
|
487
972
|
}
|
|
488
973
|
} catch (err) {
|
|
489
|
-
spinner2.
|
|
974
|
+
spinner2.error("Failed to generate QR code");
|
|
490
975
|
const msg = err instanceof Error ? err.message : String(err);
|
|
491
|
-
|
|
976
|
+
p.log.error(msg);
|
|
492
977
|
process.exit(1);
|
|
493
978
|
}
|
|
494
|
-
|
|
979
|
+
p.outro(`${DIM}Done!${RESET}`);
|
|
495
980
|
}
|
|
496
981
|
|
|
497
|
-
// src/
|
|
982
|
+
// src/commands/batch.ts
|
|
498
983
|
var RESET2 = "\x1B[0m";
|
|
499
|
-
var BOLD = "\x1B[1m";
|
|
500
|
-
var DIM2 = "\x1B[38;5;102m";
|
|
501
984
|
var TEXT2 = "\x1B[38;5;145m";
|
|
985
|
+
var GREEN2 = "\x1B[38;5;114m";
|
|
986
|
+
var DIM2 = "\x1B[38;5;102m";
|
|
987
|
+
var BATCH_FLAGS = /* @__PURE__ */ new Set([
|
|
988
|
+
"-o",
|
|
989
|
+
"--out",
|
|
990
|
+
"--output",
|
|
991
|
+
"--value-column",
|
|
992
|
+
"--name-column",
|
|
993
|
+
"--jobs"
|
|
994
|
+
]);
|
|
995
|
+
function parseBatchArgs(args) {
|
|
996
|
+
const options = { out: "qr-codes", header: true, style: [] };
|
|
997
|
+
for (let i = 0; i < args.length; i++) {
|
|
998
|
+
const arg = args[i];
|
|
999
|
+
const next = args[i + 1];
|
|
1000
|
+
if (arg === "-h" || arg === "--help") {
|
|
1001
|
+
options.help = true;
|
|
1002
|
+
} else if (arg === "-v" || arg === "--version") {
|
|
1003
|
+
options.version = true;
|
|
1004
|
+
} else if (arg === "--no-header") {
|
|
1005
|
+
options.header = false;
|
|
1006
|
+
} else if (BATCH_FLAGS.has(arg)) {
|
|
1007
|
+
if (next === void 0 || /^--?[a-z]/i.test(next)) {
|
|
1008
|
+
throw new Error(`${arg} needs a value. Run qrdx batch --help.`);
|
|
1009
|
+
}
|
|
1010
|
+
i++;
|
|
1011
|
+
if (arg === "--value-column") options.valueColumn = next;
|
|
1012
|
+
else if (arg === "--name-column") options.nameColumn = next;
|
|
1013
|
+
else if (arg === "--jobs") {
|
|
1014
|
+
const jobs = Number(next);
|
|
1015
|
+
if (!(Number.isInteger(jobs) && jobs >= 1 && jobs <= 64)) {
|
|
1016
|
+
throw new Error(
|
|
1017
|
+
`Invalid --jobs "${next}". Use a number from 1 to 64.`
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
options.jobs = jobs;
|
|
1021
|
+
} else options.out = next;
|
|
1022
|
+
} else if (!arg.startsWith("-") && options.file === void 0) {
|
|
1023
|
+
options.file = arg;
|
|
1024
|
+
} else {
|
|
1025
|
+
options.style.push(arg);
|
|
1026
|
+
if (next !== void 0 && !/^--?[a-z]/i.test(next) && !isSwitch(arg)) {
|
|
1027
|
+
options.style.push(next);
|
|
1028
|
+
i++;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return options;
|
|
1033
|
+
}
|
|
1034
|
+
var isSwitch = (arg) => arg === "--no-logo";
|
|
1035
|
+
function readableError(error) {
|
|
1036
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1037
|
+
return /too long|data too big|overflow/i.test(message) ? "Too much data for one QR code" : message || "Could not generate";
|
|
1038
|
+
}
|
|
1039
|
+
async function logoDataUrl(src) {
|
|
1040
|
+
if (src.startsWith("data:")) return src;
|
|
1041
|
+
let bytes;
|
|
1042
|
+
let type = "image/png";
|
|
1043
|
+
if (/^https?:\/\//i.test(src)) {
|
|
1044
|
+
const response = await fetch(src);
|
|
1045
|
+
if (!response.ok) {
|
|
1046
|
+
throw new Error(`Could not download logo (${response.status}): ${src}`);
|
|
1047
|
+
}
|
|
1048
|
+
type = response.headers.get("content-type")?.split(";")[0] ?? type;
|
|
1049
|
+
bytes = Buffer.from(await response.arrayBuffer());
|
|
1050
|
+
} else {
|
|
1051
|
+
bytes = readFileSync(src);
|
|
1052
|
+
if (/\.svg$/i.test(src)) type = "image/svg+xml";
|
|
1053
|
+
else if (/\.jpe?g$/i.test(src)) type = "image/jpeg";
|
|
1054
|
+
else if (/\.webp$/i.test(src)) type = "image/webp";
|
|
1055
|
+
}
|
|
1056
|
+
return `data:${type};base64,${bytes.toString("base64")}`;
|
|
1057
|
+
}
|
|
1058
|
+
async function runBatch(args, meta) {
|
|
1059
|
+
const options = parseBatchArgs(args);
|
|
1060
|
+
if (options.help) return meta.help();
|
|
1061
|
+
if (options.version) return meta.version();
|
|
1062
|
+
if (!options.file) {
|
|
1063
|
+
throw new Error(
|
|
1064
|
+
"Give a CSV file: qrdx batch codes.csv -o qr-codes (run qrdx batch --help)."
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
const { flags, settings } = styleFromArgs(options.style);
|
|
1068
|
+
if (flags.value !== void 0) {
|
|
1069
|
+
throw new Error(
|
|
1070
|
+
`Unexpected argument "${flags.value}". qrdx batch takes one CSV file.`
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
const format = flags.format ?? "svg";
|
|
1074
|
+
if (settings.logo) settings.logo = await logoDataUrl(settings.logo);
|
|
1075
|
+
const out = resolve(options.out);
|
|
1076
|
+
mkdirSync(out, { recursive: true });
|
|
1077
|
+
const jobs = options.jobs ?? (format === "svg" ? 4 : 8);
|
|
1078
|
+
const allocate = createFilenameAllocator();
|
|
1079
|
+
const failures = [];
|
|
1080
|
+
const running = /* @__PURE__ */ new Set();
|
|
1081
|
+
let columns = options.header ? null : [];
|
|
1082
|
+
let valueColumn = 0;
|
|
1083
|
+
let nameColumn = -1;
|
|
1084
|
+
let line = 0;
|
|
1085
|
+
let saved = 0;
|
|
1086
|
+
let index = 0;
|
|
1087
|
+
const started = performance.now();
|
|
1088
|
+
const tty = process.stderr.isTTY === true;
|
|
1089
|
+
let lastReport = 0;
|
|
1090
|
+
const report = (force = false) => {
|
|
1091
|
+
const now = performance.now();
|
|
1092
|
+
if (!force && now - lastReport < (tty ? 100 : 5e3)) return;
|
|
1093
|
+
lastReport = now;
|
|
1094
|
+
const text2 = `${index.toLocaleString("en-US")} rows \xB7 ${saved.toLocaleString("en-US")} saved${failures.length > 0 ? ` \xB7 ${failures.length.toLocaleString("en-US")} skipped` : ""}`;
|
|
1095
|
+
process.stderr.write(tty ? `\r${DIM2}${text2}${RESET2}\x1B[K` : `${text2}
|
|
1096
|
+
`);
|
|
1097
|
+
};
|
|
1098
|
+
const render = (value, file, row) => {
|
|
1099
|
+
const task = saveOutput({
|
|
1100
|
+
...buildQrProps(value, settings),
|
|
1101
|
+
outputPath: join(out, file),
|
|
1102
|
+
format
|
|
1103
|
+
}).then(() => {
|
|
1104
|
+
saved++;
|
|
1105
|
+
}).catch((error) => {
|
|
1106
|
+
failures.push({ row, value, error: readableError(error) });
|
|
1107
|
+
}).finally(() => {
|
|
1108
|
+
running.delete(task);
|
|
1109
|
+
report();
|
|
1110
|
+
});
|
|
1111
|
+
running.add(task);
|
|
1112
|
+
};
|
|
1113
|
+
for await (const row of readCsvRows(options.file)) {
|
|
1114
|
+
line++;
|
|
1115
|
+
if (columns === null) {
|
|
1116
|
+
columns = row.map((name, i) => name.trim() || `Column ${i + 1}`);
|
|
1117
|
+
valueColumn = options.valueColumn ? resolveColumn(options.valueColumn, columns, "--value-column") : guessValueColumn(columns);
|
|
1118
|
+
nameColumn = options.nameColumn ? resolveColumn(options.nameColumn, columns, "--name-column") : guessFilenameColumn(columns, valueColumn);
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
if (line === 1 && !options.header) {
|
|
1122
|
+
const width = row.length;
|
|
1123
|
+
const numbered = Array.from({ length: width }, (_, i) => String(i + 1));
|
|
1124
|
+
if (options.valueColumn) {
|
|
1125
|
+
valueColumn = resolveColumn(
|
|
1126
|
+
options.valueColumn,
|
|
1127
|
+
numbered,
|
|
1128
|
+
"--value-column"
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
if (options.nameColumn) {
|
|
1132
|
+
nameColumn = resolveColumn(
|
|
1133
|
+
options.nameColumn,
|
|
1134
|
+
numbered,
|
|
1135
|
+
"--name-column"
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
index++;
|
|
1140
|
+
const value = row[valueColumn]?.trim() ?? "";
|
|
1141
|
+
if (!value) {
|
|
1142
|
+
failures.push({ row: line, value, error: "Empty value" });
|
|
1143
|
+
report();
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
const base = sanitizeFilename(nameColumn >= 0 ? row[nameColumn] ?? "" : "") || `qr-${String(index).padStart(5, "0")}`;
|
|
1147
|
+
render(value, allocate(base, format), line);
|
|
1148
|
+
if (running.size >= jobs) await Promise.race(running);
|
|
1149
|
+
}
|
|
1150
|
+
await Promise.all(running);
|
|
1151
|
+
report(true);
|
|
1152
|
+
if (tty) process.stderr.write("\n");
|
|
1153
|
+
if (failures.length > 0) {
|
|
1154
|
+
failures.sort((a, b) => a.row - b.row);
|
|
1155
|
+
writeFileSync(
|
|
1156
|
+
join(out, "errors.csv"),
|
|
1157
|
+
`${["row,value,error", ...failures.map((f) => [f.row, f.value, f.error].map(csvCell).join(","))].join("\n")}
|
|
1158
|
+
`,
|
|
1159
|
+
"utf-8"
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
const seconds = ((performance.now() - started) / 1e3).toFixed(1);
|
|
1163
|
+
if (saved === 0) {
|
|
1164
|
+
throw new Error(
|
|
1165
|
+
index === 0 ? "The CSV has no rows with codes." : `No codes could be made. See ${join(out, "errors.csv")}.`
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
console.log(
|
|
1169
|
+
`${GREEN2}\u2714${RESET2} Saved ${saved.toLocaleString("en-US")} ${format.toUpperCase()} codes to ${TEXT2}${out}${RESET2} ${DIM2}in ${seconds} s${RESET2}`
|
|
1170
|
+
);
|
|
1171
|
+
if (failures.length > 0) {
|
|
1172
|
+
console.log(
|
|
1173
|
+
` ${failures.length.toLocaleString("en-US")} rows skipped: ${TEXT2}${join(out, "errors.csv")}${RESET2}`
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// src/index.ts
|
|
1179
|
+
var RESET3 = "\x1B[0m";
|
|
1180
|
+
var BOLD = "\x1B[1m";
|
|
1181
|
+
var DIM3 = "\x1B[38;5;102m";
|
|
1182
|
+
var TEXT3 = "\x1B[38;5;145m";
|
|
502
1183
|
var GRAYS = [
|
|
503
1184
|
"\x1B[38;5;250m",
|
|
504
1185
|
"\x1B[38;5;248m",
|
|
@@ -517,8 +1198,8 @@ var LOGO_LINES = [
|
|
|
517
1198
|
];
|
|
518
1199
|
function getVersion() {
|
|
519
1200
|
try {
|
|
520
|
-
const pkgPath =
|
|
521
|
-
const pkg = JSON.parse(
|
|
1201
|
+
const pkgPath = join(import.meta.dirname, "..", "package.json");
|
|
1202
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
522
1203
|
return pkg.version;
|
|
523
1204
|
} catch {
|
|
524
1205
|
return "0.0.1";
|
|
@@ -528,84 +1209,116 @@ var VERSION = getVersion();
|
|
|
528
1209
|
function showLogo() {
|
|
529
1210
|
console.log();
|
|
530
1211
|
LOGO_LINES.forEach((line, i) => {
|
|
531
|
-
console.log(`${GRAYS[i] ?? GRAYS[5]}${line}${
|
|
1212
|
+
console.log(`${GRAYS[i] ?? GRAYS[5]}${line}${RESET3}`);
|
|
532
1213
|
});
|
|
533
1214
|
}
|
|
534
1215
|
function showBanner() {
|
|
535
1216
|
showLogo();
|
|
536
1217
|
console.log();
|
|
537
|
-
console.log(`${
|
|
1218
|
+
console.log(`${DIM3}QR code generator for developers${RESET3}`);
|
|
538
1219
|
console.log();
|
|
539
1220
|
console.log(
|
|
540
|
-
` ${
|
|
1221
|
+
` ${DIM3}$${RESET3} ${TEXT3}qrdx generate${RESET3} ${DIM3}Generate a QR code interactively${RESET3}`
|
|
541
1222
|
);
|
|
542
1223
|
console.log(
|
|
543
|
-
` ${
|
|
1224
|
+
` ${DIM3}$${RESET3} ${TEXT3}qrdx generate${RESET3} ${DIM3}<url>${RESET3} ${DIM3}Generate from URL${RESET3}`
|
|
1225
|
+
);
|
|
1226
|
+
console.log(
|
|
1227
|
+
` ${DIM3}$${RESET3} ${TEXT3}qrdx batch${RESET3} ${DIM3}<file.csv>${RESET3} ${DIM3}A code for every row of a CSV${RESET3}`
|
|
544
1228
|
);
|
|
545
1229
|
console.log();
|
|
546
1230
|
console.log(
|
|
547
|
-
` ${
|
|
1231
|
+
` ${DIM3}$${RESET3} ${TEXT3}qrdx --help${RESET3} ${DIM3}Show all commands and options${RESET3}`
|
|
548
1232
|
);
|
|
549
1233
|
console.log(
|
|
550
|
-
` ${
|
|
1234
|
+
` ${DIM3}$${RESET3} ${TEXT3}qrdx --version${RESET3} ${DIM3}Show version${RESET3}`
|
|
551
1235
|
);
|
|
552
1236
|
console.log();
|
|
553
|
-
console.log(` ${
|
|
1237
|
+
console.log(` ${DIM3}try:${RESET3} npx @qrdx/cli generate "https://qrdx.dev"`);
|
|
554
1238
|
console.log();
|
|
555
1239
|
}
|
|
556
1240
|
function showHelp() {
|
|
557
1241
|
showLogo();
|
|
558
1242
|
console.log(`
|
|
559
|
-
${BOLD}Usage:${
|
|
1243
|
+
${BOLD}Usage:${RESET3} qrdx <command> [options]
|
|
560
1244
|
|
|
561
|
-
${BOLD}Commands:${
|
|
1245
|
+
${BOLD}Commands:${RESET3}
|
|
562
1246
|
generate, gen, g Generate a QR code (fully interactive or via flags)
|
|
1247
|
+
batch, b A static QR code for every row of a CSV file
|
|
563
1248
|
|
|
564
|
-
${BOLD}Generate Options:${
|
|
1249
|
+
${BOLD}Generate Options:${RESET3}
|
|
565
1250
|
<data> URL or text to encode (positional)
|
|
566
|
-
-o, --output Output file path (.svg or .
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
${
|
|
570
|
-
|
|
571
|
-
--
|
|
572
|
-
${
|
|
573
|
-
${
|
|
574
|
-
--
|
|
575
|
-
${
|
|
576
|
-
${
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
--
|
|
582
|
-
--
|
|
583
|
-
--
|
|
584
|
-
--
|
|
1251
|
+
-o, --output Output file path (.svg, .png, .webp or .eps)
|
|
1252
|
+
--format Output format when the file name has no extension:
|
|
1253
|
+
${DIM3}svg, png, webp, eps${RESET3} ${DIM3}(WebP is lossless; EPS is vector,${RESET3}
|
|
1254
|
+
${DIM3}--size = width in points)${RESET3}
|
|
1255
|
+
-l, --level Error correction: L, M, Q, H ${DIM3}(default: Q)${RESET3}
|
|
1256
|
+
--body Body dot pattern, e.g.:
|
|
1257
|
+
${DIM3}square, rounded, circle, circle-large, liquid,${RESET3}
|
|
1258
|
+
${DIM3}classy, classy-rounded, leaf, hexagon, star, heart${RESET3}
|
|
1259
|
+
--eye Corner eye pattern, e.g.:
|
|
1260
|
+
${DIM3}square, rounded, extra-rounded, gear, leaf, drop,${RESET3}
|
|
1261
|
+
${DIM3}squircle, octagon, shield, petal${RESET3}
|
|
1262
|
+
--dot Corner dot pattern, e.g.:
|
|
1263
|
+
${DIM3}square, rounded, circle, leaf, drop, squircle,${RESET3}
|
|
1264
|
+
${DIM3}octagon, flower, heart, plus, dots${RESET3}
|
|
1265
|
+
${DIM3}All options: https://qrdx.dev/docs/styles${RESET3}
|
|
1266
|
+
--fg Foreground color \u2014 hex or JSON gradient ${DIM3}(default: #000000)${RESET3}
|
|
1267
|
+
--bg Background color \u2014 hex or JSON gradient ${DIM3}(default: #ffffff)${RESET3}
|
|
1268
|
+
--eye-color Corner eye color \u2014 hex or JSON gradient ${DIM3}(default: matches --fg)${RESET3}
|
|
1269
|
+
--dot-color Corner dot color \u2014 hex or JSON gradient ${DIM3}(default: matches --fg)${RESET3}
|
|
1270
|
+
--logo Logo URL for center image (use with --level H)
|
|
1271
|
+
--no-logo Skip the logo prompt in interactive mode
|
|
1272
|
+
--margin Quiet zone margin in modules ${DIM3}(default: 0)${RESET3}
|
|
1273
|
+
--size Output pixel size for SVG/PNG/WebP ${DIM3}(default: 512)${RESET3}
|
|
585
1274
|
--template Decorative template wrapper:
|
|
586
|
-
${
|
|
1275
|
+
${DIM3}Arrow, StandardBox, SquareBorder, StrikedBox, Halloween${RESET3}
|
|
1276
|
+
--frame Call-to-action frame (adds space around the code), e.g.:
|
|
1277
|
+
${DIM3}none, banner-bottom, banner-top, rounded-border,${RESET3}
|
|
1278
|
+
${DIM3}speech-bubble, polaroid, phone, ticket, sticker${RESET3}
|
|
1279
|
+
${DIM3}All frames: https://qrdx.dev/docs/styles${RESET3}
|
|
1280
|
+
--frame-text Frame label, up to 24 characters ${DIM3}(default: Scan me; "" hides it)${RESET3}
|
|
1281
|
+
--frame-color Frame color \u2014 hex ${DIM3}(default: #000000)${RESET3}
|
|
1282
|
+
--frame-text-color Frame label color \u2014 hex ${DIM3}(default: white on banners)${RESET3}
|
|
1283
|
+
${DIM3}Frame text/colors without --frame use banner-bottom${RESET3}
|
|
1284
|
+
|
|
1285
|
+
${BOLD}Batch Options:${RESET3} ${DIM3}qrdx batch <file.csv> [options]${RESET3}
|
|
1286
|
+
-o, --out Folder for the codes ${DIM3}(default: qr-codes)${RESET3}
|
|
1287
|
+
--format svg, png or eps ${DIM3}(default: svg; PNG needs sharp)${RESET3}
|
|
1288
|
+
--value-column Column with each code's content, by name or number
|
|
1289
|
+
${DIM3}(default: value, url, link, data, content, text or qr; else the first)${RESET3}
|
|
1290
|
+
--name-column Column with file names, by name or number
|
|
1291
|
+
${DIM3}(default: file name, name, label, id, title or sku; else qr-00001, ...)${RESET3}
|
|
1292
|
+
--no-header The first row is a code, not column names
|
|
1293
|
+
--jobs Codes rendered at a time ${DIM3}(default: 4 for SVG, 8 otherwise)${RESET3}
|
|
1294
|
+
Every style option above applies to all codes. Rows that fail are listed
|
|
1295
|
+
in errors.csv in the output folder. No row limit; nothing is uploaded.
|
|
587
1296
|
|
|
588
|
-
${BOLD}Gradient Colors:${
|
|
1297
|
+
${BOLD}Gradient Colors:${RESET3}
|
|
589
1298
|
Pass a JSON object to --fg / --bg / --eye-color / --dot-color for gradients:
|
|
590
|
-
${
|
|
591
|
-
${
|
|
1299
|
+
${DIM3}Linear: '{"type":"linear","stops":[{"color":"#f00","offset":0},{"color":"#00f","offset":100}],"angle":45}'${RESET3}
|
|
1300
|
+
${DIM3}Radial: '{"type":"radial","stops":[{"color":"#f00","offset":0},{"color":"#00f","offset":100}]}'${RESET3}
|
|
1301
|
+
${DIM3}Angular: '{"type":"angular","stops":[{"color":"#f00","offset":0},{"color":"#00f","offset":100}],"angle":0}'${RESET3}
|
|
592
1302
|
|
|
593
|
-
${BOLD}Global Options:${
|
|
1303
|
+
${BOLD}Global Options:${RESET3}
|
|
594
1304
|
--help, -h Show this help message
|
|
595
1305
|
--version, -v Show version number
|
|
596
1306
|
|
|
597
|
-
${BOLD}Examples:${
|
|
598
|
-
${
|
|
599
|
-
${
|
|
600
|
-
${
|
|
601
|
-
${
|
|
602
|
-
${
|
|
603
|
-
${
|
|
604
|
-
${
|
|
605
|
-
${
|
|
606
|
-
${
|
|
1307
|
+
${BOLD}Examples:${RESET3}
|
|
1308
|
+
${DIM3}$${RESET3} qrdx generate
|
|
1309
|
+
${DIM3}$${RESET3} qrdx generate "https://qrdx.dev"
|
|
1310
|
+
${DIM3}$${RESET3} qrdx generate "https://qrdx.dev" -o qr.svg
|
|
1311
|
+
${DIM3}$${RESET3} qrdx generate "https://qrdx.dev" -o qr.png --body circle --eye gear --dot flower
|
|
1312
|
+
${DIM3}$${RESET3} qrdx generate "https://qrdx.dev" -o qr.eps --size 144
|
|
1313
|
+
${DIM3}$${RESET3} qrdx generate "https://qrdx.dev" --fg "#ff0000" --bg "#ffffff" --size 1024
|
|
1314
|
+
${DIM3}$${RESET3} qrdx generate "https://qrdx.dev" --logo "https://qrdx.dev/logo.png" --level H -o branded.svg
|
|
1315
|
+
${DIM3}$${RESET3} qrdx generate "https://qrdx.dev" --template Arrow -o arrow.svg
|
|
1316
|
+
${DIM3}$${RESET3} qrdx generate "https://qrdx.dev" --template Halloween --body circle --eye gear -o halloween.svg
|
|
1317
|
+
${DIM3}$${RESET3} qrdx generate "https://qrdx.dev" --frame banner-bottom --frame-text "Scan for the menu" -o menu.png
|
|
1318
|
+
${DIM3}$${RESET3} qrdx batch products.csv -o codes --format png --size 1024 --body circle
|
|
1319
|
+
${DIM3}$${RESET3} qrdx generate "https://qrdx.dev" --fg '{"type":"linear","stops":[{"color":"#6366f1","offset":0},{"color":"#ec4899","offset":100}],"angle":135}' -o gradient.svg
|
|
607
1320
|
|
|
608
|
-
Visit ${
|
|
1321
|
+
Visit ${TEXT3}https://qrdx.dev${RESET3} for more.
|
|
609
1322
|
`);
|
|
610
1323
|
}
|
|
611
1324
|
async function main() {
|
|
@@ -616,13 +1329,23 @@ async function main() {
|
|
|
616
1329
|
}
|
|
617
1330
|
const command = args[0];
|
|
618
1331
|
const restArgs = args.slice(1);
|
|
1332
|
+
const generateMeta = {
|
|
1333
|
+
help: showHelp,
|
|
1334
|
+
version: () => console.log(VERSION),
|
|
1335
|
+
start: () => {
|
|
1336
|
+
showLogo();
|
|
1337
|
+
console.log();
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
619
1340
|
switch (command) {
|
|
620
1341
|
case "generate":
|
|
621
1342
|
case "gen":
|
|
622
1343
|
case "g":
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
1344
|
+
await runGenerate(restArgs, generateMeta);
|
|
1345
|
+
break;
|
|
1346
|
+
case "batch":
|
|
1347
|
+
case "b":
|
|
1348
|
+
await runBatch(restArgs, generateMeta);
|
|
626
1349
|
break;
|
|
627
1350
|
case "--help":
|
|
628
1351
|
case "-h":
|
|
@@ -635,17 +1358,16 @@ async function main() {
|
|
|
635
1358
|
break;
|
|
636
1359
|
default:
|
|
637
1360
|
if (command.startsWith("-")) {
|
|
638
|
-
console.
|
|
639
|
-
console.
|
|
1361
|
+
console.error(`Unknown command: ${command}`);
|
|
1362
|
+
console.error(`Run ${BOLD}qrdx --help${RESET3} for usage.`);
|
|
1363
|
+
process.exitCode = 1;
|
|
640
1364
|
} else {
|
|
641
|
-
|
|
642
|
-
console.log();
|
|
643
|
-
await runGenerate(args);
|
|
1365
|
+
await runGenerate(args, generateMeta);
|
|
644
1366
|
}
|
|
645
1367
|
}
|
|
646
1368
|
}
|
|
647
1369
|
main().catch((err) => {
|
|
648
1370
|
const msg = err instanceof Error ? err.message : String(err);
|
|
649
|
-
console.error(`\x1B[38;5;203m\u2717 ${msg}${
|
|
1371
|
+
console.error(`\x1B[38;5;203m\u2717 ${msg}${RESET3}`);
|
|
650
1372
|
process.exit(1);
|
|
651
1373
|
});
|