hcc2d-encoder 0.0.0-stage → 0.9.0-rc.1

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/src/cli.js ADDED
@@ -0,0 +1,156 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { basename } from "node:path";
3
+ import {
4
+ PALETTE_4_MODEL1,
5
+ PALETTE_4_MODEL2,
6
+ PALETTE_8_MODEL1,
7
+ PALETTE_8_MODEL2,
8
+ buildHcc2dfPayload,
9
+ encodeHcc2d,
10
+ encodeQr,
11
+ writePng,
12
+ } from "./encoder.js";
13
+
14
+ const PROGRAM_INFO = `HCC2D Encoder - pure JavaScript reference implementation
15
+ Copyright 2010-2026 Marco Querini | SPDX-License-Identifier: Apache-2.0 | Version 0.9.0-rc.1
16
+
17
+ Specification compliance:
18
+ Intended to conform to the HCC2D Code Specification version 0.9.0.
19
+ Reference specification: https://doi.org/10.5281/zenodo.22206528
20
+
21
+ Description:
22
+ This encoder is provided for educational purposes. It is a compact standalone
23
+ reference implementation and is unrelated to the HCC2D Encoder and HCC2D
24
+ Decoder desktop/mobile apps written in C/C++.
25
+
26
+ Terms of service / no-warranty notice:
27
+ This software is provided "as is", without warranties or conditions of any
28
+ kind, express or implied, including but not limited to merchantability,
29
+ fitness for a particular purpose, and noninfringement. Use is entirely at
30
+ your own risk. The author assumes no responsibility or liability for damages,
31
+ losses, claims, unreadable or non-decodable codes, failed scans, data loss,
32
+ or other consequences arising from its use.
33
+
34
+ Apache 2.0 summary:
35
+ You may use, copy, modify, and redistribute this software, including for
36
+ commercial purposes. You must retain the applicable copyright and license
37
+ notices and state significant modifications when redistributing modified
38
+ versions. This summary is informational only; the LICENSE file and Apache
39
+ License 2.0 text control the legal terms.
40
+ Full license text: https://www.apache.org/licenses/LICENSE-2.0`;
41
+
42
+ export const HELP = `Usage: hcc2d-encoder [options] output.png
43
+
44
+ ${PROGRAM_INFO}
45
+
46
+ Payload source (exactly one is required):
47
+ --text STRING UTF-8 string to encode as the payload
48
+ --input-file PATH File bytes to encode
49
+
50
+ Options:
51
+ --file-format FORMAT Payload format: hcc2df wraps text or file bytes with
52
+ filename and compression metadata; raw encodes the
53
+ supplied bytes directly (default: hcc2df)
54
+ --mode MODE Symbol mode: qr = standard black/white QR Code;
55
+ hcc2d4 = 4 colours, 2 bits per data module; hcc2d8 =
56
+ 8 colours, 3 bits per data module. For qr,
57
+ --file-format changes only the stored payload bytes
58
+ (default: hcc2d4)
59
+ --ec-level LEVEL Error-correction level: L ~ 7%, M ~ 15%, Q ~ 25%,
60
+ or H ~ 30% recoverable damage. Higher levels reduce
61
+ data capacity (default: Q)
62
+ --version N Symbol version 1-40. Version v has an inner grid of
63
+ (17+4v) x (17+4v) modules; 0 selects the smallest
64
+ version that fits the payload (default: 0)
65
+ --scale PX Pixels per module in the output PNG, 6-1000
66
+ (default: 12)
67
+ --quiet-zone N White margin outside the symbol, in modules, 0-1000
68
+ (default: 4)
69
+ --palette MODEL HCC2D palette: model1 = screen display; model2 =
70
+ print/CMYK; ignored for qr (default: model1)
71
+ -h, --help Show this help and exit
72
+ -V, --version-info Show the package version and exit
73
+
74
+ Examples:
75
+ hcc2d-encoder --text "hello" --mode qr --file-format raw qr.png
76
+ hcc2d-encoder --text "hello" --mode hcc2d4 hcc2d4.png
77
+ hcc2d-encoder --input-file photo.jpg --mode hcc2d8 --file-format hcc2df hcc2d8.png`;
78
+
79
+ const VALUE_OPTIONS = new Set(["--text", "--input-file", "--file-format", "--mode", "--ec-level", "--version", "--scale", "--quiet-zone", "--palette"]);
80
+
81
+ export function parseArguments(argv) {
82
+ const options = {
83
+ text: null, inputFile: null, fileFormat: "hcc2df", mode: "hcc2d4", ecLevel: "Q",
84
+ version: 0, scale: 12, quietZone: 4, palette: "model1", output: null,
85
+ };
86
+ const names = {
87
+ "--text": "text", "--input-file": "inputFile", "--file-format": "fileFormat", "--mode": "mode",
88
+ "--ec-level": "ecLevel", "--version": "version", "--scale": "scale", "--quiet-zone": "quietZone", "--palette": "palette",
89
+ };
90
+ for (let index = 0; index < argv.length; index += 1) {
91
+ const argument = argv[index];
92
+ if (argument === "-h" || argument === "--help") return { help: true };
93
+ if (argument === "-V" || argument === "--version-info") return { versionInfo: true };
94
+ if (argument.startsWith("--")) {
95
+ if (!VALUE_OPTIONS.has(argument)) throw new RangeError(`unknown option: ${argument}`);
96
+ if (index + 1 >= argv.length) throw new RangeError(`${argument} requires a value`);
97
+ const key = names[argument];
98
+ const raw = argv[++index];
99
+ options[key] = ["version", "scale", "quietZone"].includes(key) ? Number(raw) : raw;
100
+ } else if (options.output === null) options.output = argument;
101
+ else throw new RangeError(`unexpected argument: ${argument}`);
102
+ }
103
+ if ((options.text === null) === (options.inputFile === null)) throw new RangeError("exactly one of --text and --input-file is required");
104
+ if (!options.output) throw new RangeError("output PNG path is required");
105
+ if (!["hcc2df", "raw"].includes(options.fileFormat)) throw new RangeError("file format must be hcc2df or raw");
106
+ if (!["qr", "hcc2d4", "hcc2d8"].includes(options.mode)) throw new RangeError("mode must be qr, hcc2d4, or hcc2d8");
107
+ if (!["L", "M", "Q", "H"].includes(options.ecLevel)) throw new RangeError("EC level must be one of L/M/Q/H");
108
+ if (!["model1", "model2"].includes(options.palette)) throw new RangeError("palette must be model1 or model2");
109
+ if (!Number.isInteger(options.version) || options.version < 0 || options.version > 40) throw new RangeError("version must be an integer from 0 to 40");
110
+ if (!Number.isInteger(options.scale) || options.scale < 6 || options.scale > 1000) throw new RangeError("scale must be an integer from 6 to 1000");
111
+ if (!Number.isInteger(options.quietZone) || options.quietZone < 0 || options.quietZone > 1000) throw new RangeError("quiet zone must be an integer from 0 to 1000");
112
+ return options;
113
+ }
114
+
115
+ function friendlyError(error, options, payload) {
116
+ const message = error instanceof Error ? error.message : String(error);
117
+ if (message.includes("payload does not fit in any supported version") && options) {
118
+ const payloadSize = payload?.length ?? "unknown";
119
+ return `Error: with mode=${options.mode} and ec_level=${options.ecLevel} the payload (${payloadSize} bytes) does not fit in any supported symbol. Try a lower EC level or a higher-capacity mode.`;
120
+ }
121
+ return `Error: ${message}`;
122
+ }
123
+
124
+ export function main(argv = process.argv.slice(2), io = console) {
125
+ if (argv.length === 0) { io.log(HELP); return 0; }
126
+ let options;
127
+ let payload;
128
+ try {
129
+ options = parseArguments(argv);
130
+ if (options.help) { io.log(HELP); return 0; }
131
+ if (options.versionInfo) { io.log("0.9.0-rc.1"); return 0; }
132
+ if (!Number.isInteger(options.version) || options.version < 0 || options.version > 40) throw new RangeError("version must be an integer from 0 to 40");
133
+ if (!Number.isInteger(options.scale) || options.scale < 6 || options.scale > 1000) throw new RangeError("scale must be an integer from 6 to 1000");
134
+ if (!Number.isInteger(options.quietZone) || options.quietZone < 0 || options.quietZone > 1000) throw new RangeError("quiet zone must be an integer from 0 to 1000");
135
+ const source = options.text !== null ? Buffer.from(options.text, "utf8") : readFileSync(options.inputFile);
136
+ if (source.length === 0) throw new RangeError("payload is empty");
137
+ const sourceName = options.text !== null ? "text.txt" : basename(options.inputFile);
138
+ const sourceKind = options.text !== null ? "text" : "file";
139
+ payload = options.fileFormat === "hcc2df" ? buildHcc2dfPayload(sourceName, source) : source;
140
+ const common = { ecLevel: options.ecLevel, version: options.version, scale: options.scale, quietZone: options.quietZone };
141
+ let symbol;
142
+ if (options.mode === "qr") symbol = encodeQr(payload, common);
143
+ else {
144
+ const palettes = options.mode === "hcc2d4"
145
+ ? { model1: PALETTE_4_MODEL1, model2: PALETTE_4_MODEL2 }
146
+ : { model1: PALETTE_8_MODEL1, model2: PALETTE_8_MODEL2 };
147
+ symbol = encodeHcc2d(payload, { ...common, mode: options.mode, palette: palettes[options.palette] });
148
+ }
149
+ writePng(options.output, symbol);
150
+ io.log(`wrote ${options.output} mode=${symbol.mode} ec=${symbol.ecLevel} version=${symbol.version} mask=${symbol.maskPattern} palette=${options.palette} size=${symbol.width}x${symbol.height} payload=${options.fileFormat}-${sourceKind}${options.fileFormat === "raw" ? "-bytes" : ""}`);
151
+ return 0;
152
+ } catch (error) {
153
+ io.error(friendlyError(error, options, payload));
154
+ return 1;
155
+ }
156
+ }