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/CHANGELOG.md +19 -0
- package/LICENSE +186 -0
- package/README.md +311 -2
- package/bin/hcc2d-encoder.js +5 -0
- package/package.json +51 -4
- package/src/cli.js +156 -0
- package/src/encoder.js +646 -0
- package/src/index.js +15 -0
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
|
+
}
|