@qrdx/cli 0.0.5 → 0.0.7
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/README.md +18 -8
- package/dist/index.js +497 -100
- package/package.json +18 -16
package/README.md
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
# @qrdx/cli
|
|
2
2
|
|
|
3
|
-
QR code generator CLI for developers. Generate styled, scannable QR codes as SVG, PNG or vector EPS. It runs non-interactively in scripts and CI, and guides you with prompts when run in a terminal. It renders with the same [`qrdx`](https://www.npmjs.com/package/qrdx) library as the [QRdx](https://qrdx.dev) web app.
|
|
4
|
-
|
|
5
|
-
> [!NOTE]
|
|
6
|
-
> This README documents `@qrdx/cli` 0.0.5, the version in this repository. npm still has 0.0.4, so `npx @qrdx/cli` runs 0.0.4 until 0.0.5 is published. 0.0.4 has the qrdx 0.0.46 styles and no `--format` or `--frame` options, and it writes SVG into a file named `qr.eps`. For EPS today, use the [image API](https://qrdx.dev/docs/developers/image-api): `curl -o qr.eps "https://api.qrdx.dev/v1/qr?data=https://example.com&format=eps"`. For frames, download from the [playground](https://qrdx.dev/playground). Run `npm view @qrdx/cli version` to see what npm installs.
|
|
3
|
+
QR code generator CLI for developers. Generate styled, scannable QR codes as SVG, PNG, WebP or vector EPS. It runs non-interactively in scripts and CI, and guides you with prompts when run in a terminal. It renders with the same [`qrdx`](https://www.npmjs.com/package/qrdx) library as the [QRdx](https://qrdx.dev) web app.
|
|
7
4
|
|
|
8
5
|
## Install
|
|
9
6
|
|
|
@@ -20,7 +17,7 @@ npm install -g @qrdx/cli
|
|
|
20
17
|
qrdx generate "https://example.com" -o qr.svg
|
|
21
18
|
```
|
|
22
19
|
|
|
23
|
-
PNG output
|
|
20
|
+
PNG and WebP output use [sharp](https://sharp.pixelplumbing.com). It is an optional dependency that npm installs automatically when your platform supports it; if it is missing, SVG and EPS output still work and the CLI tells you how to fix PNG and WebP.
|
|
24
21
|
|
|
25
22
|
> The `qrdx` npm package is the React library. The CLI lives only in `@qrdx/cli`.
|
|
26
23
|
|
|
@@ -36,9 +33,9 @@ qrdx generate <data> [options]
|
|
|
36
33
|
|
|
37
34
|
| Flag | Short | Default | Description |
|
|
38
35
|
|------|-------|---------|-------------|
|
|
39
|
-
| `--output` | `-o` | `qr.svg` | Output path; the extension sets the format (`.svg`, `.png` or `.eps`) |
|
|
40
|
-
| `--format` | | from the extension | `svg`, `png` or `eps`, for a file name without an extension |
|
|
41
|
-
| `--size` | | `512` | Output size: pixels for SVG and
|
|
36
|
+
| `--output` | `-o` | `qr.svg` | Output path; the extension sets the format (`.svg`, `.png`, `.webp` or `.eps`) |
|
|
37
|
+
| `--format` | | from the extension | `svg`, `png`, `webp` or `eps`, for a file name without an extension |
|
|
38
|
+
| `--size` | | `512` | Output size: pixels for SVG, PNG and WebP, points (72 per inch) for EPS |
|
|
42
39
|
| `--level` | `-l` | `Q` | Error correction: `L` `M` `Q` `H` |
|
|
43
40
|
| `--body` | | `square` | Body dot pattern |
|
|
44
41
|
| `--eye` | | `square` | Corner eye pattern |
|
|
@@ -69,6 +66,9 @@ qrdx generate "https://qrdx.dev" -o qr.svg
|
|
|
69
66
|
# High-res PNG for print
|
|
70
67
|
qrdx generate "https://qrdx.dev" -o qr.png --size 2048 --level H
|
|
71
68
|
|
|
69
|
+
# Lossless WebP for a website: the PNG's pixels in a smaller file
|
|
70
|
+
qrdx generate "https://qrdx.dev" -o qr.webp --size 1024
|
|
71
|
+
|
|
72
72
|
# Vector EPS for a print shop or Illustrator, 2 inches wide
|
|
73
73
|
qrdx generate "https://qrdx.dev" -o qr.eps --size 144
|
|
74
74
|
|
|
@@ -100,6 +100,16 @@ qrdx generate "WIFI:T:WPA;S:MyNetwork;P:MyPassword;;" -o wifi.svg
|
|
|
100
100
|
qrdx generate
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
+
## Batch: a code for every row of a CSV
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
qrdx batch products.csv -o codes --format png --size 1024 --body circle
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`qrdx batch` (or `qrdx b`) reads a CSV and writes one static QR code per row into a folder, with the same columns as the free [bulk generator](https://qrdx.dev/tools/bulk). The content comes from a column named `value`, `url`, `link`, `data`, `content`, `text` or `qr` (else the first column), and file names from one named `file name`, `name`, `label`, `id`, `title` or `sku` (else `qr-00001.svg`, `qr-00002.svg` and so on). Pick others with `--value-column` and `--name-column`, by header name or column number, and use `--no-header` when the first row is a code.
|
|
110
|
+
|
|
111
|
+
Every style option of `generate` applies to all the codes. The file is read as a stream and each code is written as soon as it's made, so there's no row limit; nothing is uploaded and no account is needed. Rows that can't be encoded are skipped and listed, with the reason, in `errors.csv` in the output folder. `--jobs` sets how many codes render at a time (4 for SVG, 8 for PNG and EPS by default).
|
|
112
|
+
|
|
103
113
|
## Patterns
|
|
104
114
|
|
|
105
115
|
**Body** (`--body`): <!-- catalogue:BODY_PATTERN -->`circle`, `circle-large`, `square`, `diamond`, `circle-mixed`, `pacman`, `rounded`, `small-square`, `vertical-line`, `horizontal-line`, `liquid`, `liquid-horizontal`, `liquid-vertical`, `classy`, `classy-rounded`, `leaf`, `plus`, `hexagon`, `star`, `heart`, `random-dots`, `mosaic`, `octagon`<!-- /catalogue -->
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,144 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync,
|
|
3
|
-
import { join, extname } from 'path';
|
|
4
|
-
import * as readline from 'readline';
|
|
5
|
-
import * as p from '@clack/prompts';
|
|
6
|
-
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';
|
|
2
|
+
import { readFileSync, mkdirSync, writeFileSync, createReadStream } from 'fs';
|
|
3
|
+
import { join, resolve, extname } from 'path';
|
|
7
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';
|
|
8
6
|
import { createElement } from 'react';
|
|
9
7
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
8
|
+
import * as readline from 'readline';
|
|
9
|
+
import * as p from '@clack/prompts';
|
|
10
10
|
|
|
11
|
-
var
|
|
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
|
+
}
|
|
43
|
+
}
|
|
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);
|
|
84
|
+
}
|
|
85
|
+
yield* parser.end();
|
|
86
|
+
}
|
|
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();
|
|
12
142
|
function buildSVGString(props) {
|
|
13
143
|
const element = createElement(QRCodeSVG, {
|
|
14
144
|
size: props.size ?? 512,
|
|
@@ -105,24 +235,28 @@ ${svgString}`;
|
|
|
105
235
|
sharp = mod.default;
|
|
106
236
|
} catch {
|
|
107
237
|
throw new Error(
|
|
108
|
-
"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
|
|
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`
|
|
109
239
|
);
|
|
110
240
|
}
|
|
111
241
|
const svgBuffer = Buffer.from(svgString, "utf-8");
|
|
112
242
|
const size = qrProps.size ?? 512;
|
|
113
243
|
const { width, height } = getFrameDimensions(size, qrProps.frame);
|
|
114
|
-
const qrImage = sharp(svgBuffer).resize(
|
|
244
|
+
const qrImage = sharp(svgBuffer).resize(
|
|
245
|
+
Math.round(width),
|
|
246
|
+
Math.round(height)
|
|
247
|
+
);
|
|
115
248
|
const logoSrc = qrProps.imageSettings?.src;
|
|
116
249
|
const box = logoSrc ? await findLogoBox(sharp, svgString) : null;
|
|
117
250
|
if (!(logoSrc && box)) {
|
|
118
|
-
await qrImage.toFile(outputPath);
|
|
251
|
+
await encodeRaster(qrImage, format).toFile(outputPath);
|
|
119
252
|
return;
|
|
120
253
|
}
|
|
121
254
|
const logo = await sharp(await loadLogo(logoSrc)).resize(box.width, box.height, { fit: "fill" }).png().toBuffer();
|
|
122
|
-
|
|
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);
|
|
123
259
|
}
|
|
124
|
-
|
|
125
|
-
// src/commands/generate.ts
|
|
126
260
|
var RESET = "\x1B[0m";
|
|
127
261
|
var TEXT = "\x1B[38;5;145m";
|
|
128
262
|
var GREEN = "\x1B[38;5;114m";
|
|
@@ -211,9 +345,9 @@ function validateFlags(flags) {
|
|
|
211
345
|
assertOneOf("--format", flags.format, OUTPUT_FORMATS);
|
|
212
346
|
if (flags.output !== void 0) {
|
|
213
347
|
const ext = extname(flags.output).toLowerCase();
|
|
214
|
-
if (!["", ".svg", ".png", ".eps"].includes(ext)) {
|
|
348
|
+
if (!["", ".svg", ".png", ".webp", ".eps"].includes(ext)) {
|
|
215
349
|
throw new Error(
|
|
216
|
-
`Unsupported output format "${ext}". Use a .svg, .png or .eps file name.`
|
|
350
|
+
`Unsupported output format "${ext}". Use a .svg, .png, .webp or .eps file name.`
|
|
217
351
|
);
|
|
218
352
|
}
|
|
219
353
|
if (ext && flags.format && ext !== `.${flags.format}`) {
|
|
@@ -222,10 +356,13 @@ function validateFlags(flags) {
|
|
|
222
356
|
);
|
|
223
357
|
}
|
|
224
358
|
}
|
|
225
|
-
const
|
|
226
|
-
|
|
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) {
|
|
227
364
|
throw new Error(
|
|
228
|
-
`Invalid --size "${flags.size}". PNG output is at most ${MAX_PNG_SIZE} px; use .svg or .eps for larger prints.`
|
|
365
|
+
`Invalid --size "${flags.size}". ${raster === "webp" ? "WebP" : "PNG"} output is at most ${MAX_PNG_SIZE} px; use .svg or .eps for larger prints.`
|
|
229
366
|
);
|
|
230
367
|
}
|
|
231
368
|
}
|
|
@@ -235,12 +372,12 @@ function readLineFromTTY(question) {
|
|
|
235
372
|
if (process.platform === "win32") {
|
|
236
373
|
return Promise.resolve(void 0);
|
|
237
374
|
}
|
|
238
|
-
return new Promise((
|
|
375
|
+
return new Promise((resolve2) => {
|
|
239
376
|
let stream;
|
|
240
377
|
try {
|
|
241
378
|
stream = createReadStream("/dev/tty", { flags: "r" });
|
|
242
379
|
} catch {
|
|
243
|
-
|
|
380
|
+
resolve2(void 0);
|
|
244
381
|
return;
|
|
245
382
|
}
|
|
246
383
|
const rl = readline.createInterface({
|
|
@@ -250,7 +387,7 @@ function readLineFromTTY(question) {
|
|
|
250
387
|
const finish = (answer) => {
|
|
251
388
|
rl.close();
|
|
252
389
|
stream.destroy();
|
|
253
|
-
|
|
390
|
+
resolve2(answer);
|
|
254
391
|
};
|
|
255
392
|
stream.once("error", () => finish(void 0));
|
|
256
393
|
rl.question(question, (answer) => finish(answer));
|
|
@@ -373,13 +510,60 @@ function parseFlags(args) {
|
|
|
373
510
|
if (!(flags.help || flags.version)) validateFlags(flags);
|
|
374
511
|
return flags;
|
|
375
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
|
+
}
|
|
376
560
|
function isCancel2(value) {
|
|
377
561
|
return p.isCancel(value);
|
|
378
562
|
}
|
|
379
563
|
function handleCancel() {
|
|
380
564
|
p.cancel("Cancelled.");
|
|
381
565
|
console.error(
|
|
382
|
-
`${DIM}Tip:${RESET} ${TEXT}qrdx generate "https://\u2026" -o qr.svg${RESET} \u2014 local dev: ${TEXT}
|
|
566
|
+
`${DIM}Tip:${RESET} ${TEXT}qrdx generate "https://\u2026" -o qr.svg${RESET} \u2014 local dev: ${TEXT}bun run qrdx generate \u2026${RESET}`
|
|
383
567
|
);
|
|
384
568
|
process.exit(0);
|
|
385
569
|
}
|
|
@@ -388,6 +572,9 @@ function inferFormat(outputPath, format) {
|
|
|
388
572
|
if (ext === ".png") {
|
|
389
573
|
return "png";
|
|
390
574
|
}
|
|
575
|
+
if (ext === ".webp") {
|
|
576
|
+
return "webp";
|
|
577
|
+
}
|
|
391
578
|
if (ext === ".eps") {
|
|
392
579
|
return "eps";
|
|
393
580
|
}
|
|
@@ -457,7 +644,7 @@ async function runGenerate(args, meta) {
|
|
|
457
644
|
if (!outputPath) {
|
|
458
645
|
if (canUseClack) {
|
|
459
646
|
const pathRes = await p.text({
|
|
460
|
-
message: "Output file (.svg, .png or .eps)",
|
|
647
|
+
message: "Output file (.svg, .png, .webp or .eps)",
|
|
461
648
|
placeholder: defaultOutput,
|
|
462
649
|
defaultValue: defaultOutput,
|
|
463
650
|
validate(v) {
|
|
@@ -472,7 +659,7 @@ async function runGenerate(args, meta) {
|
|
|
472
659
|
outputPath = pathRes || defaultOutput;
|
|
473
660
|
} else if (process.stdout.isTTY && !isCI) {
|
|
474
661
|
const pathAns = await readLineFromTTY(
|
|
475
|
-
`Output file ${DIM}(.svg, .png or .eps, Enter = ${defaultOutput})${RESET}: `
|
|
662
|
+
`Output file ${DIM}(.svg, .png, .webp or .eps, Enter = ${defaultOutput})${RESET}: `
|
|
476
663
|
);
|
|
477
664
|
outputPath = pathAns?.trim() || defaultOutput;
|
|
478
665
|
} else {
|
|
@@ -758,29 +945,21 @@ async function runGenerate(args, meta) {
|
|
|
758
945
|
const spinner2 = p.spinner();
|
|
759
946
|
spinner2.start("Generating QR code...");
|
|
760
947
|
try {
|
|
761
|
-
const qrProps = {
|
|
762
|
-
value,
|
|
948
|
+
const qrProps = buildQrProps(value, {
|
|
763
949
|
level,
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
950
|
+
body,
|
|
951
|
+
eye,
|
|
952
|
+
dot,
|
|
953
|
+
fg,
|
|
954
|
+
bg,
|
|
955
|
+
eyeColor,
|
|
956
|
+
dotColor,
|
|
771
957
|
margin,
|
|
772
958
|
size,
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
src: logo,
|
|
778
|
-
height: Math.round(size * 0.2),
|
|
779
|
-
width: Math.round(size * 0.2),
|
|
780
|
-
excavate: true
|
|
781
|
-
}
|
|
782
|
-
} : {}
|
|
783
|
-
};
|
|
959
|
+
template,
|
|
960
|
+
frame,
|
|
961
|
+
logo
|
|
962
|
+
});
|
|
784
963
|
if (outputPath) {
|
|
785
964
|
const format = inferFormat(outputPath, flags.format);
|
|
786
965
|
await saveOutput({ ...qrProps, outputPath, format });
|
|
@@ -800,11 +979,207 @@ async function runGenerate(args, meta) {
|
|
|
800
979
|
p.outro(`${DIM}Done!${RESET}`);
|
|
801
980
|
}
|
|
802
981
|
|
|
803
|
-
// src/
|
|
982
|
+
// src/commands/batch.ts
|
|
804
983
|
var RESET2 = "\x1B[0m";
|
|
805
|
-
var BOLD = "\x1B[1m";
|
|
806
|
-
var DIM2 = "\x1B[38;5;102m";
|
|
807
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";
|
|
808
1183
|
var GRAYS = [
|
|
809
1184
|
"\x1B[38;5;250m",
|
|
810
1185
|
"\x1B[38;5;248m",
|
|
@@ -834,98 +1209,116 @@ var VERSION = getVersion();
|
|
|
834
1209
|
function showLogo() {
|
|
835
1210
|
console.log();
|
|
836
1211
|
LOGO_LINES.forEach((line, i) => {
|
|
837
|
-
console.log(`${GRAYS[i] ?? GRAYS[5]}${line}${
|
|
1212
|
+
console.log(`${GRAYS[i] ?? GRAYS[5]}${line}${RESET3}`);
|
|
838
1213
|
});
|
|
839
1214
|
}
|
|
840
1215
|
function showBanner() {
|
|
841
1216
|
showLogo();
|
|
842
1217
|
console.log();
|
|
843
|
-
console.log(`${
|
|
1218
|
+
console.log(`${DIM3}QR code generator for developers${RESET3}`);
|
|
844
1219
|
console.log();
|
|
845
1220
|
console.log(
|
|
846
|
-
` ${
|
|
1221
|
+
` ${DIM3}$${RESET3} ${TEXT3}qrdx generate${RESET3} ${DIM3}Generate a QR code interactively${RESET3}`
|
|
1222
|
+
);
|
|
1223
|
+
console.log(
|
|
1224
|
+
` ${DIM3}$${RESET3} ${TEXT3}qrdx generate${RESET3} ${DIM3}<url>${RESET3} ${DIM3}Generate from URL${RESET3}`
|
|
847
1225
|
);
|
|
848
1226
|
console.log(
|
|
849
|
-
` ${
|
|
1227
|
+
` ${DIM3}$${RESET3} ${TEXT3}qrdx batch${RESET3} ${DIM3}<file.csv>${RESET3} ${DIM3}A code for every row of a CSV${RESET3}`
|
|
850
1228
|
);
|
|
851
1229
|
console.log();
|
|
852
1230
|
console.log(
|
|
853
|
-
` ${
|
|
1231
|
+
` ${DIM3}$${RESET3} ${TEXT3}qrdx --help${RESET3} ${DIM3}Show all commands and options${RESET3}`
|
|
854
1232
|
);
|
|
855
1233
|
console.log(
|
|
856
|
-
` ${
|
|
1234
|
+
` ${DIM3}$${RESET3} ${TEXT3}qrdx --version${RESET3} ${DIM3}Show version${RESET3}`
|
|
857
1235
|
);
|
|
858
1236
|
console.log();
|
|
859
|
-
console.log(` ${
|
|
1237
|
+
console.log(` ${DIM3}try:${RESET3} npx @qrdx/cli generate "https://qrdx.dev"`);
|
|
860
1238
|
console.log();
|
|
861
1239
|
}
|
|
862
1240
|
function showHelp() {
|
|
863
1241
|
showLogo();
|
|
864
1242
|
console.log(`
|
|
865
|
-
${BOLD}Usage:${
|
|
1243
|
+
${BOLD}Usage:${RESET3} qrdx <command> [options]
|
|
866
1244
|
|
|
867
|
-
${BOLD}Commands:${
|
|
1245
|
+
${BOLD}Commands:${RESET3}
|
|
868
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
|
|
869
1248
|
|
|
870
|
-
${BOLD}Generate Options:${
|
|
1249
|
+
${BOLD}Generate Options:${RESET3}
|
|
871
1250
|
<data> URL or text to encode (positional)
|
|
872
|
-
-o, --output Output file path (.svg, .png or .eps)
|
|
1251
|
+
-o, --output Output file path (.svg, .png, .webp or .eps)
|
|
873
1252
|
--format Output format when the file name has no extension:
|
|
874
|
-
${
|
|
875
|
-
|
|
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}
|
|
876
1256
|
--body Body dot pattern, e.g.:
|
|
877
|
-
${
|
|
878
|
-
${
|
|
1257
|
+
${DIM3}square, rounded, circle, circle-large, liquid,${RESET3}
|
|
1258
|
+
${DIM3}classy, classy-rounded, leaf, hexagon, star, heart${RESET3}
|
|
879
1259
|
--eye Corner eye pattern, e.g.:
|
|
880
|
-
${
|
|
881
|
-
${
|
|
1260
|
+
${DIM3}square, rounded, extra-rounded, gear, leaf, drop,${RESET3}
|
|
1261
|
+
${DIM3}squircle, octagon, shield, petal${RESET3}
|
|
882
1262
|
--dot Corner dot pattern, e.g.:
|
|
883
|
-
${
|
|
884
|
-
${
|
|
885
|
-
${
|
|
886
|
-
--fg Foreground color \u2014 hex or JSON gradient ${
|
|
887
|
-
--bg Background color \u2014 hex or JSON gradient ${
|
|
888
|
-
--eye-color Corner eye color \u2014 hex or JSON gradient ${
|
|
889
|
-
--dot-color Corner dot color \u2014 hex or JSON gradient ${
|
|
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?ref=cli${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}
|
|
890
1270
|
--logo Logo URL for center image (use with --level H)
|
|
891
1271
|
--no-logo Skip the logo prompt in interactive mode
|
|
892
|
-
--margin Quiet zone margin in modules ${
|
|
893
|
-
--size Output pixel size for SVG/PNG ${
|
|
1272
|
+
--margin Quiet zone margin in modules ${DIM3}(default: 0)${RESET3}
|
|
1273
|
+
--size Output pixel size for SVG/PNG/WebP ${DIM3}(default: 512)${RESET3}
|
|
894
1274
|
--template Decorative template wrapper:
|
|
895
|
-
${
|
|
1275
|
+
${DIM3}Arrow, StandardBox, SquareBorder, StrikedBox, Halloween${RESET3}
|
|
896
1276
|
--frame Call-to-action frame (adds space around the code), e.g.:
|
|
897
|
-
${
|
|
898
|
-
${
|
|
899
|
-
${
|
|
900
|
-
--frame-text Frame label, up to 24 characters ${
|
|
901
|
-
--frame-color Frame color \u2014 hex ${
|
|
902
|
-
--frame-text-color Frame label color \u2014 hex ${
|
|
903
|
-
${
|
|
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?ref=cli${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.
|
|
904
1296
|
|
|
905
|
-
${BOLD}Gradient Colors:${
|
|
1297
|
+
${BOLD}Gradient Colors:${RESET3}
|
|
906
1298
|
Pass a JSON object to --fg / --bg / --eye-color / --dot-color for gradients:
|
|
907
|
-
${
|
|
908
|
-
${
|
|
909
|
-
${
|
|
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}
|
|
910
1302
|
|
|
911
|
-
${BOLD}Global Options:${
|
|
1303
|
+
${BOLD}Global Options:${RESET3}
|
|
912
1304
|
--help, -h Show this help message
|
|
913
1305
|
--version, -v Show version number
|
|
914
1306
|
|
|
915
|
-
${BOLD}Examples:${
|
|
916
|
-
${
|
|
917
|
-
${
|
|
918
|
-
${
|
|
919
|
-
${
|
|
920
|
-
${
|
|
921
|
-
${
|
|
922
|
-
${
|
|
923
|
-
${
|
|
924
|
-
${
|
|
925
|
-
${
|
|
926
|
-
${
|
|
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
|
|
927
1320
|
|
|
928
|
-
Visit ${
|
|
1321
|
+
Visit ${TEXT3}https://qrdx.dev/?ref=cli${RESET3} for more.
|
|
929
1322
|
`);
|
|
930
1323
|
}
|
|
931
1324
|
async function main() {
|
|
@@ -950,6 +1343,10 @@ async function main() {
|
|
|
950
1343
|
case "g":
|
|
951
1344
|
await runGenerate(restArgs, generateMeta);
|
|
952
1345
|
break;
|
|
1346
|
+
case "batch":
|
|
1347
|
+
case "b":
|
|
1348
|
+
await runBatch(restArgs, generateMeta);
|
|
1349
|
+
break;
|
|
953
1350
|
case "--help":
|
|
954
1351
|
case "-h":
|
|
955
1352
|
case "help":
|
|
@@ -962,7 +1359,7 @@ async function main() {
|
|
|
962
1359
|
default:
|
|
963
1360
|
if (command.startsWith("-")) {
|
|
964
1361
|
console.error(`Unknown command: ${command}`);
|
|
965
|
-
console.error(`Run ${BOLD}qrdx --help${
|
|
1362
|
+
console.error(`Run ${BOLD}qrdx --help${RESET3} for usage.`);
|
|
966
1363
|
process.exitCode = 1;
|
|
967
1364
|
} else {
|
|
968
1365
|
await runGenerate(args, generateMeta);
|
|
@@ -971,6 +1368,6 @@ async function main() {
|
|
|
971
1368
|
}
|
|
972
1369
|
main().catch((err) => {
|
|
973
1370
|
const msg = err instanceof Error ? err.message : String(err);
|
|
974
|
-
console.error(`\x1B[38;5;203m\u2717 ${msg}${
|
|
1371
|
+
console.error(`\x1B[38;5;203m\u2717 ${msg}${RESET3}`);
|
|
975
1372
|
process.exit(1);
|
|
976
1373
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qrdx/cli",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "QR code generator CLI for developers: styled SVG, PNG and vector EPS QR codes from the command line",
|
|
3
|
+
"version": "0.0.7",
|
|
4
|
+
"description": "QR code generator CLI for developers: styled SVG, PNG, WebP and vector EPS QR codes from the command line",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"qr-code",
|
|
7
7
|
"qrcode",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"cli",
|
|
10
10
|
"svg",
|
|
11
11
|
"png",
|
|
12
|
+
"webp",
|
|
12
13
|
"eps",
|
|
13
14
|
"qr-code-cli"
|
|
14
15
|
],
|
|
@@ -31,38 +32,39 @@
|
|
|
31
32
|
"dist",
|
|
32
33
|
"README.md"
|
|
33
34
|
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"qrdx": "node ./dist/index.js",
|
|
37
|
+
"build": "tsup",
|
|
38
|
+
"dev": "tsup --watch",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"test:watch": "vitest",
|
|
41
|
+
"clean": "git clean -xdf .cache .turbo dist node_modules",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"prepublishOnly": "node -e \"if(!(process.env.npm_config_user_agent||'').startsWith('bun/')){console.error('Publish through the Publish workflow, or with bun publish, which replaces workspace ranges with real versions.');process.exit(1)}\" && bun run build && bun run test && node ../qrdx/scripts/readme-catalogue.mjs --check"
|
|
44
|
+
},
|
|
34
45
|
"dependencies": {
|
|
35
46
|
"@clack/prompts": "^1.8.1",
|
|
36
47
|
"picocolors": "^1.1.1",
|
|
48
|
+
"qrdx": "0.0.49",
|
|
37
49
|
"react": "19.3.1",
|
|
38
|
-
"react-dom": "19.3.1"
|
|
39
|
-
"qrdx": "0.0.47"
|
|
50
|
+
"react-dom": "19.3.1"
|
|
40
51
|
},
|
|
41
52
|
"optionalDependencies": {
|
|
42
53
|
"sharp": "^0.35.5"
|
|
43
54
|
},
|
|
44
55
|
"devDependencies": {
|
|
56
|
+
"@repo/typescript-config": "0.0.0",
|
|
45
57
|
"@types/node": "^26.6.6",
|
|
46
58
|
"@types/react": "19.3.0",
|
|
47
59
|
"@types/react-dom": "19.3.0",
|
|
48
60
|
"tsup": "^9.0.0",
|
|
49
61
|
"typescript": "6.0.3",
|
|
50
|
-
"vitest": "^5.0.5"
|
|
51
|
-
"@repo/typescript-config": "0.0.0"
|
|
62
|
+
"vitest": "^5.0.5"
|
|
52
63
|
},
|
|
53
64
|
"engines": {
|
|
54
65
|
"node": ">=22"
|
|
55
66
|
},
|
|
56
67
|
"publishConfig": {
|
|
57
68
|
"access": "public"
|
|
58
|
-
},
|
|
59
|
-
"scripts": {
|
|
60
|
-
"qrdx": "node ./dist/index.js",
|
|
61
|
-
"build": "tsup",
|
|
62
|
-
"dev": "tsup --watch",
|
|
63
|
-
"test": "vitest run",
|
|
64
|
-
"test:watch": "vitest",
|
|
65
|
-
"clean": "git clean -xdf .cache .turbo dist node_modules",
|
|
66
|
-
"typecheck": "tsc --noEmit"
|
|
67
69
|
}
|
|
68
|
-
}
|
|
70
|
+
}
|