@rosepetal/barcode-engine-client 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/format.js ADDED
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+ /**
3
+ * The `format` name of a schema-1.0 symbol, as node-red-contrib-barcode-reader has always printed it
4
+ * (compat/barcode-reader.md §3 of rosepetal-barcode-sdk): ZXing's spelling for the symbologies; the add-ons, which
5
+ * ZXing does not report on their own, take ZBar's (EAN-2 / EAN-5). Shared here so that the reader and the verifier
6
+ * print the same `format` for the same symbol.
7
+ */
8
+
9
+ // symbology (schema 1.0 identifier) → the name the nodes print
10
+ const FORMAT_NAMES = {
11
+ EAN13: 'EAN-13', EAN8: 'EAN-8', UPCA: 'UPC-A', UPCE: 'UPC-E',
12
+ Code128: 'Code128', Code39: 'Code39', Code93: 'Code93', Codabar: 'Codabar', ITF: 'ITF'
13
+ };
14
+ // The identifier is read before the symbology: an add-on carries its main's symbology
15
+ const ADDON_NAMES = { ']E1': 'EAN-2', ']E2': 'EAN-5' };
16
+
17
+ /** The `format` the nodes print for a schema-1.0 symbol; an unknown symbology keeps the SDK's name. */
18
+ function formatName(symbol) {
19
+ return ADDON_NAMES[symbol.identifier] || FORMAT_NAMES[symbol.symbology] || String(symbol.symbology);
20
+ }
21
+
22
+ module.exports = { FORMAT_NAMES, ADDON_NAMES, formatName };
package/lib/framing.js ADDED
@@ -0,0 +1,174 @@
1
+ 'use strict';
2
+ /**
3
+ * Frame codec of the `rp-barcode serve` protocol, version 1 (docs/service.md §3.1 of rosepetal-barcode-sdk, the
4
+ * repository this package lives in; the plan: docs/plans/fase2/00-overview.md §3.1). Shared by every client of
5
+ * the engine (@rosepetal/barcode-engine-client) and by the fake engine of test/fake-engine.js:
6
+ *
7
+ * u32 big-endian headerLen | u32 big-endian payloadLen | header (headerLen bytes, UTF-8 JSON object) | payload
8
+ *
9
+ * headerLen is >= 2 and <= maxHeader (1 MiB by default), payloadLen <= maxPayload (256 MiB by default) and the
10
+ * header must be a JSON object. Anything else is a protocol violation: the parser emits one FramingError and
11
+ * ignores every later chunk, because the byte stream can no longer be delimited (the caller restarts the process).
12
+ *
13
+ * The parser copies every byte exactly once, into a buffer sized from the prefix, so a payload never lives twice
14
+ * in memory and the emitted Buffer is independent of the chunks pushed in.
15
+ */
16
+ const { EventEmitter } = require('node:events');
17
+
18
+ const PREFIX_BYTES = 8;
19
+ const MIN_HEADER = 2; // "{}"
20
+ const MAX_U32 = 0xffffffff;
21
+ const DEFAULT_MAX_HEADER = 1 << 20; // 1 MiB
22
+ const DEFAULT_MAX_PAYLOAD = 256 << 20; // 256 MiB
23
+ const EMPTY = Buffer.alloc(0);
24
+
25
+ class FramingError extends Error {
26
+ /**
27
+ * @param {'bad_frame'|'bad_header'|'bad_payload'} code
28
+ * @param {string} message
29
+ */
30
+ constructor(code, message) {
31
+ super(message);
32
+ this.name = 'FramingError';
33
+ this.code = code;
34
+ }
35
+ }
36
+
37
+ function isJsonObject(value) {
38
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
39
+ }
40
+
41
+ /**
42
+ * Encodes one frame as [head, payload]: head is the 8-byte prefix followed by the UTF-8 JSON header and payload
43
+ * is the caller's buffer untouched, so the two go out in two writes and the pixels are never copied.
44
+ *
45
+ * @param {object} header a JSON object (control characters such as GS 0x1D survive as \u001d escapes)
46
+ * @param {Buffer|Uint8Array} [payload] optional (payloadLen 0)
47
+ * @returns {[Buffer, Buffer|Uint8Array]}
48
+ */
49
+ function encodeFrame(header, payload = EMPTY) {
50
+ if (!isJsonObject(header)) {
51
+ throw new FramingError('bad_header', 'header must be a JSON object');
52
+ }
53
+ if (!(payload instanceof Uint8Array)) { // a Buffer is a Uint8Array
54
+ throw new FramingError('bad_payload', 'payload must be a Buffer or Uint8Array');
55
+ }
56
+ if (payload.length > MAX_U32) {
57
+ throw new FramingError('bad_payload', `payload length ${payload.length} does not fit in u32`);
58
+ }
59
+ const json = Buffer.from(JSON.stringify(header), 'utf8');
60
+ if (json.length > MAX_U32) {
61
+ throw new FramingError('bad_header', `header length ${json.length} does not fit in u32`);
62
+ }
63
+ const head = Buffer.allocUnsafe(PREFIX_BYTES + json.length);
64
+ head.writeUInt32BE(json.length, 0);
65
+ head.writeUInt32BE(payload.length, 4);
66
+ json.copy(head, PREFIX_BYTES);
67
+ return [head, payload];
68
+ }
69
+
70
+ /**
71
+ * Incremental parser over arbitrary chunks (any split, one byte at a time or several frames at once).
72
+ *
73
+ * Events:
74
+ * 'frame' (header: object, payload: Buffer) payload is an empty Buffer when payloadLen is 0
75
+ * 'error' (FramingError) emitted once; afterwards push() is a no-op and `failed` holds the error
76
+ */
77
+ class FrameParser extends EventEmitter {
78
+ constructor({ maxHeader = DEFAULT_MAX_HEADER, maxPayload = DEFAULT_MAX_PAYLOAD } = {}) {
79
+ super();
80
+ this.maxHeader = maxHeader;
81
+ this.maxPayload = maxPayload;
82
+ this.failed = null;
83
+ this.headerLen = 0;
84
+ this.payloadLen = 0;
85
+ this.header = null;
86
+ this.prefix = Buffer.alloc(PREFIX_BYTES);
87
+ this._expect('prefix', this.prefix);
88
+ }
89
+
90
+ /** @param {Buffer|Uint8Array} chunk */
91
+ push(chunk) {
92
+ if (this.failed) return;
93
+ if (!(chunk instanceof Uint8Array)) {
94
+ throw new TypeError('FrameParser.push expects a Buffer or Uint8Array');
95
+ }
96
+ let offset = 0;
97
+ while (offset < chunk.length) {
98
+ const n = Math.min(this.target.length - this.filled, chunk.length - offset);
99
+ this.target.set(chunk.subarray(offset, offset + n), this.filled);
100
+ this.filled += n;
101
+ offset += n;
102
+ if (this.filled === this.target.length && !this._complete()) return;
103
+ }
104
+ }
105
+
106
+ _expect(state, target) {
107
+ this.state = state;
108
+ this.target = target;
109
+ this.filled = 0;
110
+ }
111
+
112
+ /** The current target is full: validate it and move to the next state. Returns false once the parser is dead. */
113
+ _complete() {
114
+ switch (this.state) {
115
+ case 'prefix': {
116
+ this.headerLen = this.prefix.readUInt32BE(0);
117
+ this.payloadLen = this.prefix.readUInt32BE(4);
118
+ if (this.headerLen < MIN_HEADER || this.headerLen > this.maxHeader) {
119
+ return this._fail('bad_frame', `header length ${this.headerLen} outside ${MIN_HEADER}..${this.maxHeader}`);
120
+ }
121
+ if (this.payloadLen > this.maxPayload) {
122
+ return this._fail('bad_frame', `payload length ${this.payloadLen} exceeds ${this.maxPayload}`);
123
+ }
124
+ this._expect('header', Buffer.allocUnsafe(this.headerLen));
125
+ return true;
126
+ }
127
+ case 'header': {
128
+ let header;
129
+ try {
130
+ header = JSON.parse(this.target.toString('utf8'));
131
+ } catch (err) {
132
+ return this._fail('bad_header', `header is not JSON: ${err.message}`);
133
+ }
134
+ if (!isJsonObject(header)) {
135
+ return this._fail('bad_header', 'header is not a JSON object');
136
+ }
137
+ this.header = header;
138
+ if (this.payloadLen === 0) return this._emitFrame(EMPTY);
139
+ this._expect('payload', Buffer.allocUnsafe(this.payloadLen));
140
+ return true;
141
+ }
142
+ case 'payload':
143
+ return this._emitFrame(this.target);
144
+ default:
145
+ return this._fail('bad_frame', `unexpected parser state ${this.state}`);
146
+ }
147
+ }
148
+
149
+ _emitFrame(payload) {
150
+ const header = this.header;
151
+ this.header = null;
152
+ this._expect('prefix', this.prefix); // consistent state before listeners run
153
+ this.emit('frame', header, payload);
154
+ return !this.failed;
155
+ }
156
+
157
+ _fail(code, message) {
158
+ this.failed = new FramingError(code, message);
159
+ this.header = null;
160
+ this._expect('dead', EMPTY);
161
+ this.emit('error', this.failed);
162
+ return false;
163
+ }
164
+ }
165
+
166
+ module.exports = {
167
+ encodeFrame,
168
+ FrameParser,
169
+ FramingError,
170
+ PREFIX_BYTES,
171
+ MIN_HEADER,
172
+ DEFAULT_MAX_HEADER,
173
+ DEFAULT_MAX_PAYLOAD
174
+ };
package/lib/index.js ADDED
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+ /**
3
+ * @rosepetal/barcode-engine-client: the Node client of `rp-barcode serve`, the persistent decode service of
4
+ * rosepetal-barcode-sdk. One module, one shared engine: every package that requires this one and calls acquire()
5
+ * shares the same rp-barcode process of the Node-RED runtime (node-red-contrib-barcode-reader and
6
+ * node-red-contrib-barcode-verifier). See README.md.
7
+ */
8
+ const engine = require('./engine');
9
+ const framing = require('./framing');
10
+ const format = require('./format');
11
+
12
+ // The fake `rp-barcode serve` of this package's tests (protocol 1, one fixed EAN-13 per decode, the canned
13
+ // VerifyResults of test/fixtures/*.json for verify/analyze, knobs through FAKE_* environment variables), shipped so
14
+ // that the tests of the nodes can point RP_BARCODE_ENGINE, or an Engine's `command`/`args`, at it instead of a
15
+ // real binary
16
+ const FAKE_ENGINE = require.resolve('../test/fake-engine.js');
17
+
18
+ module.exports = { ...engine, ...framing, ...format, FAKE_ENGINE };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@rosepetal/barcode-engine-client",
3
+ "version": "0.2.0",
4
+ "description": "Node client of rp-barcode serve, the persistent decode and verify service of rosepetal-barcode-sdk: one shared engine process per Node-RED runtime, framed requests over stdio, timeouts, bounded queue, restart with backoff",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/rosepetal-ai/rosepetal-barcode-sdk.git",
9
+ "directory": "clients/node"
10
+ },
11
+ "main": "lib/index.js",
12
+ "files": [
13
+ "lib/",
14
+ "test/fake-engine.js",
15
+ "test/fixtures/",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "scripts": {
23
+ "test": "node --test test/*.test.js"
24
+ },
25
+ "optionalDependencies": {
26
+ "@rosepetal/barcode-engine-linux-x64": "0.2.0",
27
+ "@rosepetal/barcode-engine-linux-arm64": "0.2.0"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ }
32
+ }
@@ -0,0 +1,292 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * Fake `rp-barcode serve` for the tests: protocol 1 over stdio (rosepetal-barcode-sdk docs/service.md; the plan:
5
+ * docs/plans/fase2/00-overview.md §3, 04-subfase-2D.md §3.5), one fixed EAN-13 per decode, canned VerifyResults
6
+ * for verify/analyze (test/fixtures/*.json: what the CLI printed for the synthetic renders of
7
+ * test/tools/make-verify-fixtures.sh), knobs through environment variables (tasks 2B-T2 and 2D-T2). Executable with
8
+ * a shebang so it also works as RP_BARCODE_ENGINE (the client spawns `<path> serve`). Never matched by `npm test`
9
+ * (`test/*.test.js`).
10
+ *
11
+ * Knobs (environment of the child):
12
+ * FAKE_DELAY_MS=n every decode/verify/analyze answers after n ms
13
+ * FAKE_CRASH_AFTER=n exits with status 7 on receiving decode/verify/analyze n+1, without answering it
14
+ * FAKE_PROTOCOL=n the `protocol` number of the hello (default 1)
15
+ * FAKE_GARBAGE=1 writes text outside any frame right after the hello
16
+ * FAKE_OVERLOAD=1 answers `overloaded` to every decode/verify/analyze
17
+ * FAKE_NO_HELLO=1 never greets, never answers, ignores SIGTERM (the client has to SIGKILL)
18
+ * FAKE_IGNORE_STOP=1 works but ignores `shutdown` and SIGTERM
19
+ * FAKE_MAX_PAYLOAD=n the `limits.maxPayload` advertised in the hello (default 256 MiB, like `--max-payload`)
20
+ * FAKE_REPLY_PAYLOAD=1 attaches a payload to every decode/verify/analyze reply (a protocol violation seen from
21
+ * the client)
22
+ * FAKE_STALL=1 greets, then never reads stdin (the client's writes pile up); with FAKE_IGNORE_STOP the
23
+ * client has to SIGKILL it
24
+ * FAKE_VERIFY_STATUS=s the canned result of verify/analyze: pass (default), fail or not_detected
25
+ * FAKE_DECODE_SYMBOLS=n copies of the fixed EAN-13 a decode answers (default 1; 0 = no symbol)
26
+ * FAKE_OPS=a,b,c the ops the hello announces (default decode,ping,shutdown,verify,analyze); an op left
27
+ * out is answered `unsupported`, as by a 2A engine (an SDK 0.2.0 built before 2D)
28
+ *
29
+ * Like the real server it takes `image.encoding` `raw` (payload.length === width·height·channels) or `encoded` (a
30
+ * non-empty PNG, JPEG, BMP or TIFF file, told by its magic bytes; the size answered is the PNG's IHDR, else the
31
+ * header's width/height, where the real server reports the decoded size; with no size at all, 0×0, the bounds
32
+ * check is skipped, where the real server checks against the decoded size), rejects unknown keys inside `image`,
33
+ * `region`, `options` (`spec` included) and `acquisition` (`calibration` included) with `invalid_input` and the
34
+ * server's own wording (`json: unknown field "x"`; `plain.x` for a region value that is not an integer, as its
35
+ * encoding/json names the field), a region under 2 px, an orientation that is not a multiple of 90, a region
36
+ * outside the image (`barcode: region {…} outside image WxH`), and a `deadlineMs` that is not an integer.
37
+ * verify/analyze answer the canned result of FAKE_VERIFY_STATUS with the request's region echoed in
38
+ * `result.symbol.region` (orientation normalised to 0/90/180/270); `analyze` answers analyze-pass.json under
39
+ * `pass` and the verify fixture plus `scans: []` otherwise (the real Analyze returns the scans it measured even
40
+ * when nothing decodes).
41
+ *
42
+ * Known simplifications: `shutdown` and EOF on stdin exit at once, without answering the requests still delayed
43
+ * by FAKE_DELAY_MS (the real server finishes what is in flight first, §3.1/§3.2); keys are matched
44
+ * case-sensitively (`TryInvert` is an unknown key here, the real server's encoding/json takes it for `tryInvert`);
45
+ * the raw length message reads `imgio: data length N does not match W×H×C` (the server's: `imgio: GRAY WxH needs
46
+ * N bytes, got M`); option values are not checked against their vocabularies (`edition: "2020"` or a GS1 table
47
+ * `42` pass here and are `invalid_input` on the server); `options`, `spec`, `acquisition` and `calibration` that
48
+ * are not objects pass here (the server: `cannot unmarshal string into Go value of type barcode.VerifyOptions`).
49
+ * Logs go to stderr, one line per event.
50
+ */
51
+ const fs = require('node:fs');
52
+ const path = require('node:path');
53
+ const { FrameParser, encodeFrame } = require('../lib/framing');
54
+
55
+ const env = process.env;
56
+ const delayMs = Number(env.FAKE_DELAY_MS || 0);
57
+ const crashAfter = env.FAKE_CRASH_AFTER === undefined ? -1 : Number(env.FAKE_CRASH_AFTER);
58
+ const protocol = env.FAKE_PROTOCOL === undefined ? 1 : Number(env.FAKE_PROTOCOL);
59
+ const garbage = env.FAKE_GARBAGE === '1';
60
+ const overload = env.FAKE_OVERLOAD === '1';
61
+ const noHello = env.FAKE_NO_HELLO === '1';
62
+ const ignoreStop = env.FAKE_IGNORE_STOP === '1';
63
+ const maxPayload = env.FAKE_MAX_PAYLOAD === undefined ? 268435456 : Number(env.FAKE_MAX_PAYLOAD);
64
+ const replyPayload = env.FAKE_REPLY_PAYLOAD === '1';
65
+ const stall = env.FAKE_STALL === '1';
66
+ const verifyStatus = env.FAKE_VERIFY_STATUS || 'pass';
67
+ const decodeSymbols = env.FAKE_DECODE_SYMBOLS === undefined ? 1 : Number(env.FAKE_DECODE_SYMBOLS);
68
+ const ops = env.FAKE_OPS ? env.FAKE_OPS.split(',') : ['decode', 'ping', 'shutdown', 'verify', 'analyze'];
69
+
70
+ // The key sets the server accepts (DisallowUnknownFields on the SDK's structs): anything else is invalid_input.
71
+ // image: docs/service.md §3.2; decodeOptions: docs/schema/result-1.0.md §12.1; verifyOptions §12.2 (its spec
72
+ // {kind, table}); acquisition §12.3 and its calibration §12.4; region: docs/service.md §3.5
73
+ const KNOWN_IMAGE = new Set(['width', 'height', 'channels', 'colorSpace', 'encoding']);
74
+ const KNOWN_DECODE_OPTIONS = new Set(['symbologies', 'region', 'scanStep', 'horizontal', 'vertical', 'quietZone', 'optionalChecksum',
75
+ 'minLines', 'tryInvert', 'maxSymbols', 'addOn', 'effort', 'upcaAsEan13', 'code39FullAscii']);
76
+ const KNOWN_VERIFY_OPTIONS = new Set(['symbology', 'edition', 'scanCount', 'topMargin', 'bottomMargin', 'spec', 'failUnder', 'warnUnder',
77
+ 'readOnly', 'heightErrors', 'xDimErrors', 'structureErrors', 'enforceAssociations', 'quietZoneRule', 'parameterSummary', 'apertureMils',
78
+ 'noDenseScans']);
79
+ const KNOWN_SPEC = new Set(['kind', 'table']);
80
+ const KNOWN_ACQUISITION = new Set(['milsPerPixel', 'wavelengthNm', 'calibration']);
81
+ const KNOWN_CALIBRATION = new Set(['mode', 'black', 'white', 'cardRmax', 'cardContrast']);
82
+ const KNOWN_REGION = new Set(['x', 'y', 'w', 'h', 'orientation']);
83
+
84
+ // The canned results: what rp-barcode verify/analyze print for the synthetic renders of test/tools/make-verify-fixtures.sh
85
+ // (verify-pass, verify-fail, verify-not-detected, analyze-pass), read afresh per request so a reply never shares state
86
+ const FIXTURES = path.join(__dirname, 'fixtures');
87
+ const canned = (name) => JSON.parse(fs.readFileSync(path.join(FIXTURES, `${name}.json`), 'utf8'));
88
+
89
+ // The symbol rp-barcode decode reads from test/fixtures/ean13.png of the reader (347×68, X = 3 px, bars 60 px high);
90
+ // region is what `rp-barcode decode --robust` reports for it
91
+ const SYMBOL = {
92
+ symbology: 'EAN13', identifier: ']E0', value: '4006381333931', bytes: 'NDAwNjM4MTMzMzkzMQ==', gs1: true,
93
+ corners: [{ x: 37, y: 4 }, { x: 322, y: 4 }, { x: 322, y: 64 }, { x: 37, y: 64 }],
94
+ orientation: 0, confidence: 1, lines: 40, checksum: 'valid', region: { x: 1, y: 0, w: 346, h: 68, orientation: 0 }
95
+ };
96
+
97
+ function log(line) {
98
+ process.stderr.write(`fake-engine: ${line}\n`);
99
+ }
100
+ function write(header, payload, onDone) {
101
+ const [head, body] = encodeFrame(header, payload);
102
+ if (body.length === 0) return process.stdout.write(head, onDone);
103
+ process.stdout.write(head);
104
+ process.stdout.write(body, onDone);
105
+ }
106
+ function fail(id, code, message) {
107
+ write({ id, ok: false, error: { code, message } });
108
+ }
109
+ // A successful reply, after FAKE_DELAY_MS, with a payload under FAKE_REPLY_PAYLOAD
110
+ function reply(id, body) {
111
+ setTimeout(() => write({ id, ok: true, ...body }, replyPayload ? Buffer.from([1, 2, 3, 4]) : undefined), delayMs);
112
+ }
113
+
114
+ // What a JSON value is called in encoding/json's "cannot unmarshal … into" messages
115
+ function jsonKind(value) {
116
+ if (typeof value === 'number') return `number ${value}`;
117
+ if (typeof value === 'boolean') return 'bool';
118
+ if (Array.isArray(value)) return 'array';
119
+ return typeof value;
120
+ }
121
+ // The first key of obj outside known, as the server's decoder (DisallowUnknownFields) names it; null when all are
122
+ // known or obj is not an object
123
+ function unknownField(obj, known) {
124
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return null;
125
+ for (const key of Object.keys(obj)) {
126
+ if (!known.has(key)) return `json: unknown field ${JSON.stringify(key)}`;
127
+ }
128
+ return null;
129
+ }
130
+
131
+ // The format of an encoded payload by its magic bytes (the ones imgio decodes), or null
132
+ function imageFormat(buf) {
133
+ if (buf.length >= 8 && buf.readUInt32BE(0) === 0x89504e47) return 'png';
134
+ if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) return 'jpeg';
135
+ if (buf.length >= 2 && buf[0] === 0x42 && buf[1] === 0x4d) return 'bmp';
136
+ if (buf.length >= 4 && (buf.readUInt32BE(0) === 0x49492a00 || buf.readUInt32BE(0) === 0x4d4d002a)) return 'tiff';
137
+ return null;
138
+ }
139
+ // What the server's loadImage makes of the header and the payload: {width, height}, or {error} with its wording.
140
+ // raw: the packed rows, checked by length; encoded: a file of one of the formats above (the PNG's IHDR gives the
141
+ // size; for the others the header's width/height stand in for the decoded size the real server reports)
142
+ function loadImage(img, payload) {
143
+ img = img || {};
144
+ const unknown = unknownField(img, KNOWN_IMAGE);
145
+ if (unknown) return { error: `image: ${unknown}` };
146
+ const encoding = String(img.encoding === undefined ? '' : img.encoding).toLowerCase();
147
+ if (encoding === '' || encoding === 'raw') {
148
+ if (payload.length === 0) return { error: 'image: decode needs a raw payload of width*height*channels bytes (encoding raw)' };
149
+ const expected = (img.width | 0) * (img.height | 0) * (img.channels | 0);
150
+ if (payload.length !== expected) {
151
+ return { error: `imgio: data length ${payload.length} does not match ${img.width}×${img.height}×${img.channels}` };
152
+ }
153
+ return { width: img.width, height: img.height };
154
+ }
155
+ if (encoding === 'encoded') {
156
+ if (payload.length === 0) return { error: 'image: decode needs an encoded payload, a PNG, JPEG, BMP or TIFF file (encoding encoded)' };
157
+ const format = imageFormat(payload);
158
+ if (!format) return { error: 'imgio: image: unknown format' };
159
+ if (format === 'png' && payload.length >= 24) return { width: payload.readUInt32BE(16), height: payload.readUInt32BE(20) };
160
+ return { width: img.width | 0, height: img.height | 0 };
161
+ }
162
+ return { error: `image: unknown encoding ${JSON.stringify(img.encoding)} (raw or encoded)` };
163
+ }
164
+
165
+ // The server's region checks (RegionSpec.UnmarshalJSON, regionFrom; docs/service.md §3.5) with their wording:
166
+ // {error}, or the region with its orientation normalised to 0/90/180/270. The bounds are checked against the image
167
+ // once it is loaded, as Verify does
168
+ function checkRegion(region) {
169
+ if (region === undefined || region === null) {
170
+ return { error: 'region: required for verify and analyze ({x, y, w, h, orientation} in image pixels)' };
171
+ }
172
+ if (typeof region !== 'object' || Array.isArray(region)) {
173
+ // the type without the value, as encoding/json names a whole value of the wrong kind
174
+ return { error: `region: json: cannot unmarshal ${typeof region === 'number' ? 'number' : jsonKind(region)} into Go value of type serve.plain` };
175
+ }
176
+ const unknown = unknownField(region, KNOWN_REGION);
177
+ if (unknown) return { error: `region: ${unknown}` };
178
+ for (const key of KNOWN_REGION) {
179
+ const value = region[key];
180
+ if (value !== undefined && value !== null && !Number.isInteger(value)) {
181
+ return { error: `region: json: cannot unmarshal ${jsonKind(value)} into Go struct field plain.${key} of type int` };
182
+ }
183
+ }
184
+ const [x, y, w, h, o] = ['x', 'y', 'w', 'h', 'orientation'].map((key) => region[key] || 0);
185
+ if (w < 2 || h < 2) return { error: `region: width ${w} and height ${h} must be positive and at least 2 px` };
186
+ if (o % 90 !== 0) return { error: `region: orientation ${o} is not a multiple of 90` };
187
+ return { x, y, w, h, orientation: ((o % 360) + 360) % 360 };
188
+ }
189
+
190
+ process.stdout.on('error', () => process.exit(1)); // broken pipe: the parent is gone (§3.1: status 1)
191
+
192
+ if (!['pass', 'fail', 'not_detected'].includes(verifyStatus)) {
193
+ log(`FAKE_VERIFY_STATUS ${JSON.stringify(verifyStatus)} is not pass, fail or not_detected`);
194
+ process.exit(2);
195
+ }
196
+
197
+ if (noHello) {
198
+ process.on('SIGTERM', () => { /* ignored: the client has to SIGKILL */ });
199
+ process.stdin.resume(); // reads and never answers
200
+ } else {
201
+ if (ignoreStop) process.on('SIGTERM', () => { /* ignored */ });
202
+ write({
203
+ id: 0, op: 'hello', protocol, sdk: '0.2.0-fake', engineVersion: '0.2.0+fake', schema: '1.0',
204
+ ops, workers: 1, queue: 64,
205
+ limits: { maxHeader: 1048576, maxPayload, maxSide: 32768, maxPixels: 268435456 }
206
+ });
207
+ log(`hello sent (pid ${process.pid})`);
208
+ if (garbage) process.stdout.write('this is not a frame\n');
209
+
210
+ // FAKE_CRASH_AFTER counts decode, verify and analyze alike: request n+1 dies mid-flight, without an answer
211
+ let requests = 0;
212
+ function countOrCrash() {
213
+ if (crashAfter >= 0 && requests >= crashAfter) process.exit(7);
214
+ requests += 1;
215
+ }
216
+
217
+ const parser = new FrameParser({ maxPayload });
218
+ parser.on('error', (err) => {
219
+ fail(0, 'bad_frame', err.message);
220
+ log(`bad frame: ${err.message}`);
221
+ process.exit(3);
222
+ });
223
+ parser.on('frame', (req, payload) => {
224
+ // As the binary's parseRequest: a non-integer deadlineMs fails the whole request before its op is looked at
225
+ if (req.deadlineMs !== undefined && req.deadlineMs !== null && !Number.isInteger(req.deadlineMs)) {
226
+ const what = typeof req.deadlineMs === 'number' ? `number ${req.deadlineMs}` : typeof req.deadlineMs;
227
+ return fail(req.id, 'invalid_input', `json: cannot unmarshal ${what} into Go struct field Request.deadlineMs of type int`);
228
+ }
229
+ switch (req.op) {
230
+ case 'ping':
231
+ write({ id: req.id, ok: true });
232
+ break;
233
+ case 'shutdown':
234
+ if (ignoreStop) break;
235
+ write({ id: req.id, ok: true }, undefined, () => process.exit(0));
236
+ break;
237
+ case 'decode': {
238
+ countOrCrash();
239
+ if (overload) return fail(req.id, 'overloaded', 'queue full');
240
+ const img = loadImage(req.image, payload);
241
+ if (img.error) return fail(req.id, 'invalid_input', img.error);
242
+ const unknown = unknownField(req.options, KNOWN_DECODE_OPTIONS);
243
+ if (unknown) return fail(req.id, 'invalid_input', `options: ${unknown}`);
244
+ reply(req.id, {
245
+ image: img,
246
+ symbols: Array.from({ length: decodeSymbols }, () => ({ ...SYMBOL, region: { ...SYMBOL.region } })),
247
+ timing: { totalMs: delayMs, decodeMs: delayMs, queuedMs: 0 }
248
+ });
249
+ break;
250
+ }
251
+ case 'verify':
252
+ case 'analyze': {
253
+ // The server's order (handleVerify): region, options, acquisition, image, then Verify's bounds check
254
+ if (!ops.includes(req.op)) return fail(req.id, 'unsupported', `op ${JSON.stringify(req.op)} is not supported (ops: ${ops.join(', ')})`);
255
+ countOrCrash();
256
+ if (overload) return fail(req.id, 'overloaded', 'queue full');
257
+ const region = checkRegion(req.region);
258
+ if (region.error) return fail(req.id, 'invalid_input', region.error);
259
+ const unknownOpt = unknownField(req.options, KNOWN_VERIFY_OPTIONS) || unknownField(req.options && req.options.spec, KNOWN_SPEC);
260
+ if (unknownOpt) return fail(req.id, 'invalid_input', `options: ${unknownOpt}`);
261
+ const acq = req.acquisition || {};
262
+ const unknownAcq = unknownField(acq, KNOWN_ACQUISITION) || unknownField(acq.calibration, KNOWN_CALIBRATION);
263
+ if (unknownAcq) return fail(req.id, 'invalid_input', `acquisition: ${unknownAcq}`);
264
+ const img = loadImage(req.image, payload);
265
+ if (img.error) return fail(req.id, 'invalid_input', img.error);
266
+ // Verify's bounds check, when the size is known (an encoded payload that is not a PNG and declares no size is 0×0 here)
267
+ if (img.width > 0 && img.height > 0 && (region.x < 0 || region.y < 0 || region.x + region.w > img.width || region.y + region.h > img.height)) {
268
+ return fail(req.id, 'invalid_input', `barcode: region {X:${region.x} Y:${region.y} W:${region.w} H:${region.h} ` +
269
+ `Orientation:${region.orientation}} outside image ${img.width}x${img.height}`);
270
+ }
271
+ let result;
272
+ if (verifyStatus === 'pass') {
273
+ result = canned(req.op === 'analyze' ? 'analyze-pass' : 'verify-pass');
274
+ } else {
275
+ result = canned(verifyStatus === 'fail' ? 'verify-fail' : 'verify-not-detected');
276
+ if (req.op === 'analyze') result.scans = [];
277
+ }
278
+ if (result.symbol) result.symbol.region = region;
279
+ reply(req.id, { image: img, result, timing: { totalMs: delayMs, verifyMs: delayMs, queuedMs: 0 } });
280
+ break;
281
+ }
282
+ default:
283
+ fail(req.id, 'unsupported', `op ${JSON.stringify(req.op)} is not supported (ops: ${ops.join(', ')})`);
284
+ }
285
+ });
286
+ if (stall) {
287
+ setInterval(() => { /* alive, deaf: stdin is never read */ }, 1 << 30);
288
+ } else {
289
+ process.stdin.on('data', (chunk) => parser.push(chunk));
290
+ process.stdin.on('end', () => process.exit(0)); // EOF: the parent closed the pipe (§3.1: status 0)
291
+ }
292
+ }
@@ -0,0 +1 @@
1
+ {"status":"pass","reasons":[],"warnings":[],"symbol":{"symbology":"EAN13","identifier":"]E0","value":"4006381333931","bytes":"NDAwNjM4MTMzMzkzMQ==","gs1":true,"corners":[{"x":37,"y":4},{"x":322,"y":4},{"x":322,"y":64},{"x":37,"y":64}],"orientation":0,"confidence":1,"lines":60,"checksum":"valid","region":{"x":4,"y":4,"w":339,"h":60,"orientation":0}},"quality":{"overall":{"value":0,"grade":4,"letter":"A","worst":0,"mean":0,"hasValue":true},"text":"4.0/00/660","decode":{"pass":true,"value":60,"detail":""},"decodedLines":60,"edgeDetermination":{"pass":true,"value":0,"detail":""},"minReflectance":{"pass":true,"value":5.098,"detail":""},"minEdgeContrast":{"pass":true,"value":80,"detail":""},"symbolContrast":{"value":80,"grade":4,"letter":"A","worst":4,"mean":80,"hasValue":true},"modulation":{"value":1,"grade":4,"letter":"A","worst":4,"mean":1,"hasValue":true},"defects":{"value":0,"grade":4,"letter":"A","worst":4,"mean":0,"hasValue":true},"decodability":{"value":1,"grade":4,"letter":"A","worst":4,"mean":1,"hasValue":true},"rmin":5.098,"rmax":85.098,"gt":45.098,"quietZone":{"leftX":11,"rightX":7,"leftPct":110,"rightPct":112.9032,"requiredLeftX":10,"requiredRightX":6.2,"pass":true},"additional":{"pass":true,"value":0,"detail":""},"decodabilityProvisional":false,"inconsistent":false,"limiting":"","scanGrades":[4,4,4]},"dimensions":{"evaluated":false,"xDimPx":3,"xDimMils":0,"xDimMm":0,"nominalXMm":0,"magnification":0,"barHeightPx":60,"barHeightMm":0,"barGrowthPct":0,"narrowBarPx":3,"narrowSpacePx":3,"symbolWidthPx":285,"quietZoneLeftX":11,"quietZoneRightX":7,"ratio":0},"gs1":{"identifier":"]E0","hri":"(01)04006381333931","elements":[{"ai":"01","value":"04006381333931","title":"GTIN"}],"structure":"pass","dataStructure":"pass","errors":[],"table":"","limits":[],"dictionary":"2026-01-27"},"application":{"spec":{"kind":"iso","table":""},"tableTitle":"","limitAction":"","apertureRef":0,"apertureMils":0,"wavelengthNm":660,"edition":"2016","calibration":"none"},"version":{"sdk":"0.2.0","schema":"1.0","dictionary":"2026-01-27"},"timing":{"totalMs":2.411,"measureMs":1.258,"decodeMs":0.4,"gradeMs":0.012,"gs1Ms":0.714},"report":[{"category":0,"name":"Overall grade","value":"4.0/00/660"},{"category":2,"name":"Symbology","value":"EAN-13"},{"category":2,"name":"Decoded text","value":"4006381333931"},{"category":2,"name":"Xdim","value":"3.00 px"},{"category":2,"name":"Edge determ","value":"PASS"},{"category":2,"name":"Min Reflect","value":"PASS"},{"category":2,"name":"Minimum EC","value":"PASS"},{"category":2,"name":"Decode","value":"PASS 60"},{"category":2,"name":"Quiet zone","value":"PASS"},{"category":2,"name":"Contrast","value":"4.0 80%"},{"category":2,"name":"Modulation","value":"4.0 100%"},{"category":2,"name":"Decodability","value":"4.0 100%"},{"category":2,"name":"Defects","value":"4.0 0%"},{"category":2,"name":"Rmin","value":"5.1%"},{"category":2,"name":"Rmax","value":"85.1%"},{"category":2,"name":"GT","value":"45.1%"},{"category":2,"name":"Bar height","value":"60.0 px"},{"category":2,"name":"Bar growth","value":"0%"},{"category":2,"name":"Structure","value":"PASS"},{"category":6,"name":"GS1 Data","value":"(01)04006381333931"},{"category":6,"name":"GS1 Data Structure Pass/Fail","value":"PASS"},{"category":6,"name":"Letter Grade","value":"A"},{"category":6,"name":"Application standard","value":"ISO/IEC 15416"},{"category":6,"name":"Effective aperture","value":"n/a"},{"category":6,"name":"Lighting","value":"660"},{"category":6,"name":"Software product and version","value":"rosepetal-barcode-sdk 0.2.0 (schema 1.0)"}],"scans":[{"index":0,"y":14,"grade":4,"decoded":true,"value":"4006381333931","reason":"","rmax":85.098,"rmin":5.098,"gt":45.098,"sc":80,"ecmin":80,"mod":1,"ernmax":0,"defects":0,"decodability":1,"quietZoneLeftX":11,"quietZoneRightX":7,"profile":[85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098],"edges":[33,36,39,42,51,57,60,63,66,69,75,84,87,90,93,105,108,120,123,126,135,138,144,147,150,156,162,168,171,174,177,180,183,186,198,201,204,207,219,222,225,228,240,243,246,255,258,261,267,270,282,285,288,294,300,306,309,312,315,318]},{"index":1,"y":30,"grade":4,"decoded":true,"value":"4006381333931","reason":"","rmax":85.098,"rmin":5.098,"gt":45.098,"sc":80,"ecmin":80,"mod":1,"ernmax":0,"defects":0,"decodability":1,"quietZoneLeftX":11,"quietZoneRightX":7,"profile":[85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098],"edges":[33,36,39,42,51,57,60,63,66,69,75,84,87,90,93,105,108,120,123,126,135,138,144,147,150,156,162,168,171,174,177,180,183,186,198,201,204,207,219,222,225,228,240,243,246,255,258,261,267,270,282,285,288,294,300,306,309,312,315,318]},{"index":2,"y":46,"grade":4,"decoded":true,"value":"4006381333931","reason":"","rmax":85.098,"rmin":5.098,"gt":45.098,"sc":80,"ecmin":80,"mod":1,"ernmax":0,"defects":0,"decodability":1,"quietZoneLeftX":11,"quietZoneRightX":7,"profile":[85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,77.098,45.098,13.098,5.098,5.098,5.098,13.098,45.098,77.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098,85.098],"edges":[33,36,39,42,51,57,60,63,66,69,75,84,87,90,93,105,108,120,123,126,135,138,144,147,150,156,162,168,171,174,177,180,183,186,198,201,204,207,219,222,225,228,240,243,246,255,258,261,267,270,282,285,288,294,300,306,309,312,315,318]}]}
@@ -0,0 +1 @@
1
+ {"status":"fail","reasons":["overall grade 0.0 below fail threshold 1.5 (limiting: Min Reflect)"],"warnings":[],"symbol":{"symbology":"Code128","identifier":"]C0","value":"ROSEPETAL-ISO","bytes":"Uk9TRVBFVEFMLUlTTw==","gs1":false,"corners":[{"x":44,"y":4},{"x":756,"y":4},{"x":756,"y":64},{"x":44,"y":64}],"orientation":0,"confidence":1,"lines":60,"checksum":"valid","region":{"x":4,"y":4,"w":792,"h":60,"orientation":0}},"quality":{"overall":{"value":0,"grade":0,"letter":"F","worst":0,"mean":0,"hasValue":true},"text":"0.0/00/660","decode":{"pass":true,"value":60,"detail":""},"decodedLines":60,"edgeDetermination":{"pass":true,"value":0,"detail":""},"minReflectance":{"pass":false,"value":70.1961,"detail":""},"minEdgeContrast":{"pass":false,"value":9.8039,"detail":""},"symbolContrast":{"value":9.8039,"grade":0,"letter":"F","worst":0,"mean":9.8039,"hasValue":true},"modulation":{"value":1,"grade":4,"letter":"A","worst":4,"mean":1,"hasValue":true},"defects":{"value":0,"grade":4,"letter":"A","worst":4,"mean":0,"hasValue":true},"decodability":{"value":1,"grade":4,"letter":"A","worst":4,"mean":1,"hasValue":true},"rmin":70.1961,"rmax":80,"gt":75.098,"quietZone":{"leftX":10,"rightX":10,"leftPct":100,"rightPct":100,"requiredLeftX":10,"requiredRightX":10,"pass":true},"additional":{"pass":true,"value":0,"detail":""},"decodabilityProvisional":false,"inconsistent":false,"limiting":"MinReflectance","scanGrades":[0,0,0,0,0,0,0,0,0,0]},"dimensions":{"evaluated":false,"xDimPx":4,"xDimMils":0,"xDimMm":0,"nominalXMm":0,"magnification":0,"barHeightPx":60,"barHeightMm":0,"barGrowthPct":0,"narrowBarPx":4,"narrowSpacePx":4,"symbolWidthPx":712,"quietZoneLeftX":10,"quietZoneRightX":10,"ratio":0},"gs1":null,"application":{"spec":{"kind":"iso","table":""},"tableTitle":"","limitAction":"","apertureRef":0,"apertureMils":0,"wavelengthNm":660,"edition":"2016","calibration":"none"},"version":{"sdk":"0.2.0","schema":"1.0","dictionary":"2026-01-27"},"timing":{"totalMs":7.238,"measureMs":5.092,"decodeMs":0.971,"gradeMs":0.024,"gs1Ms":1.126},"report":[{"category":0,"name":"Overall grade","value":"0.0/00/660"},{"category":2,"name":"Symbology","value":"Code 128"},{"category":2,"name":"Decoded text","value":"ROSEPETAL-ISO"},{"category":2,"name":"Xdim","value":"4.00 px"},{"category":2,"name":"Edge determ","value":"PASS"},{"category":2,"name":"Min Reflect","value":"FAIL"},{"category":2,"name":"Minimum EC","value":"FAIL"},{"category":2,"name":"Decode","value":"PASS 60"},{"category":2,"name":"Quiet zone","value":"PASS"},{"category":2,"name":"Contrast","value":"0.0 9.8%"},{"category":2,"name":"Modulation","value":"4.0 100%"},{"category":2,"name":"Decodability","value":"4.0 100%"},{"category":2,"name":"Defects","value":"4.0 0%"},{"category":2,"name":"Rmin","value":"70.2%"},{"category":2,"name":"Rmax","value":"80%"},{"category":2,"name":"GT","value":"75.1%"},{"category":2,"name":"Bar height","value":"60.0 px"},{"category":2,"name":"Bar growth","value":"0%"},{"category":2,"name":"Structure","value":"PASS"},{"category":6,"name":"Letter Grade","value":"F"},{"category":6,"name":"Application standard","value":"ISO/IEC 15416"},{"category":6,"name":"Effective aperture","value":"n/a"},{"category":6,"name":"Lighting","value":"660"},{"category":6,"name":"Software product and version","value":"rosepetal-barcode-sdk 0.2.0 (schema 1.0)"}]}
@@ -0,0 +1 @@
1
+ {"status":"not_detected","reasons":["no decode"],"warnings":[],"symbol":null,"quality":{"overall":{"value":0,"grade":0,"letter":"F","worst":0,"mean":0,"hasValue":true},"text":"0.0/00/660","decode":{"pass":false,"value":0,"detail":""},"decodedLines":0,"edgeDetermination":{"pass":false,"value":0,"detail":""},"minReflectance":{"pass":false,"value":59.473,"detail":""},"minEdgeContrast":{"pass":false,"value":12.8125,"detail":""},"symbolContrast":{"value":12.8125,"grade":0,"letter":"F","worst":0,"mean":12.8125,"hasValue":true},"modulation":{"value":0.5,"grade":0,"letter":"F","worst":0,"mean":0.5,"hasValue":true},"defects":{"value":0,"grade":0,"letter":"F","worst":0,"mean":0,"hasValue":true},"decodability":{"value":0,"grade":0,"letter":"","worst":0,"mean":0,"hasValue":false},"rmin":59.473,"rmax":72.2855,"gt":65.8793,"quietZone":{"leftX":0,"rightX":0,"leftPct":0,"rightPct":0,"requiredLeftX":0,"requiredRightX":0,"pass":false},"additional":{"pass":false,"value":0,"detail":""},"decodabilityProvisional":false,"inconsistent":false,"limiting":"Decode","scanGrades":[0,0,0,0,0,0,0,0,0,0]},"dimensions":{"evaluated":false,"xDimPx":0,"xDimMils":0,"xDimMm":0,"nominalXMm":0,"magnification":0,"barHeightPx":0,"barHeightMm":0,"barGrowthPct":0,"narrowBarPx":0,"narrowSpacePx":0,"symbolWidthPx":0,"quietZoneLeftX":0,"quietZoneRightX":0,"ratio":0},"gs1":null,"application":{"spec":{"kind":"iso","table":""},"tableTitle":"","limitAction":"","apertureRef":0,"apertureMils":0,"wavelengthNm":660,"edition":"2016","calibration":"none"},"version":{"sdk":"0.2.0","schema":"1.0","dictionary":"2026-01-27"},"timing":{"totalMs":23.855,"measureMs":23.767,"decodeMs":0.061,"gradeMs":0.022,"gs1Ms":0},"report":[{"category":0,"name":"Overall grade","value":"Bar Code Not Detected"},{"category":2,"name":"Xdim","value":"n/a"},{"category":2,"name":"Edge determ","value":"n/a"},{"category":2,"name":"Min Reflect","value":"FAIL"},{"category":2,"name":"Minimum EC","value":"FAIL"},{"category":2,"name":"Decode","value":"FAIL"},{"category":2,"name":"Quiet zone","value":"n/a"},{"category":2,"name":"Contrast","value":"0.0 12.8%"},{"category":2,"name":"Modulation","value":"0.0 50%"},{"category":2,"name":"Decodability","value":"n/a"},{"category":2,"name":"Defects","value":"0.0 0%"},{"category":2,"name":"Rmin","value":"59.5%"},{"category":2,"name":"Rmax","value":"72.3%"},{"category":2,"name":"GT","value":"65.9%"},{"category":2,"name":"Bar height","value":"n/a"},{"category":2,"name":"Bar growth","value":"n/a"},{"category":2,"name":"Structure","value":"n/a"},{"category":6,"name":"Letter Grade","value":"F"},{"category":6,"name":"Application standard","value":"ISO/IEC 15416"},{"category":6,"name":"Effective aperture","value":"n/a"},{"category":6,"name":"Lighting","value":"660"},{"category":6,"name":"Software product and version","value":"rosepetal-barcode-sdk 0.2.0 (schema 1.0)"}]}
@@ -0,0 +1 @@
1
+ {"status":"pass","reasons":[],"warnings":[],"symbol":{"symbology":"EAN13","identifier":"]E0","value":"4006381333931","bytes":"NDAwNjM4MTMzMzkzMQ==","gs1":true,"corners":[{"x":37,"y":4},{"x":322,"y":4},{"x":322,"y":64},{"x":37,"y":64}],"orientation":0,"confidence":1,"lines":60,"checksum":"valid","region":{"x":4,"y":4,"w":339,"h":60,"orientation":0}},"quality":{"overall":{"value":0,"grade":4,"letter":"A","worst":0,"mean":0,"hasValue":true},"text":"4.0/00/660","decode":{"pass":true,"value":60,"detail":""},"decodedLines":60,"edgeDetermination":{"pass":true,"value":0,"detail":""},"minReflectance":{"pass":true,"value":5.098,"detail":""},"minEdgeContrast":{"pass":true,"value":80,"detail":""},"symbolContrast":{"value":80,"grade":4,"letter":"A","worst":4,"mean":80,"hasValue":true},"modulation":{"value":1,"grade":4,"letter":"A","worst":4,"mean":1,"hasValue":true},"defects":{"value":0,"grade":4,"letter":"A","worst":4,"mean":0,"hasValue":true},"decodability":{"value":1,"grade":4,"letter":"A","worst":4,"mean":1,"hasValue":true},"rmin":5.098,"rmax":85.098,"gt":45.098,"quietZone":{"leftX":11,"rightX":7,"leftPct":110,"rightPct":112.9032,"requiredLeftX":10,"requiredRightX":6.2,"pass":true},"additional":{"pass":true,"value":0,"detail":""},"decodabilityProvisional":false,"inconsistent":false,"limiting":"","scanGrades":[4,4,4,4,4,4,4,4,4,4]},"dimensions":{"evaluated":false,"xDimPx":3,"xDimMils":0,"xDimMm":0,"nominalXMm":0,"magnification":0,"barHeightPx":60,"barHeightMm":0,"barGrowthPct":0,"narrowBarPx":3,"narrowSpacePx":3,"symbolWidthPx":285,"quietZoneLeftX":11,"quietZoneRightX":7,"ratio":0},"gs1":{"identifier":"]E0","hri":"(01)04006381333931","elements":[{"ai":"01","value":"04006381333931","title":"GTIN"}],"structure":"pass","dataStructure":"pass","errors":[],"table":"","limits":[],"dictionary":"2026-01-27"},"application":{"spec":{"kind":"iso","table":""},"tableTitle":"","limitAction":"","apertureRef":0,"apertureMils":0,"wavelengthNm":660,"edition":"2016","calibration":"none"},"version":{"sdk":"0.2.0","schema":"1.0","dictionary":"2026-01-27"},"timing":{"totalMs":3.515,"measureMs":2.238,"decodeMs":0.526,"gradeMs":0.026,"gs1Ms":0.706},"report":[{"category":0,"name":"Overall grade","value":"4.0/00/660"},{"category":2,"name":"Symbology","value":"EAN-13"},{"category":2,"name":"Decoded text","value":"4006381333931"},{"category":2,"name":"Xdim","value":"3.00 px"},{"category":2,"name":"Edge determ","value":"PASS"},{"category":2,"name":"Min Reflect","value":"PASS"},{"category":2,"name":"Minimum EC","value":"PASS"},{"category":2,"name":"Decode","value":"PASS 60"},{"category":2,"name":"Quiet zone","value":"PASS"},{"category":2,"name":"Contrast","value":"4.0 80%"},{"category":2,"name":"Modulation","value":"4.0 100%"},{"category":2,"name":"Decodability","value":"4.0 100%"},{"category":2,"name":"Defects","value":"4.0 0%"},{"category":2,"name":"Rmin","value":"5.1%"},{"category":2,"name":"Rmax","value":"85.1%"},{"category":2,"name":"GT","value":"45.1%"},{"category":2,"name":"Bar height","value":"60.0 px"},{"category":2,"name":"Bar growth","value":"0%"},{"category":2,"name":"Structure","value":"PASS"},{"category":6,"name":"GS1 Data","value":"(01)04006381333931"},{"category":6,"name":"GS1 Data Structure Pass/Fail","value":"PASS"},{"category":6,"name":"Letter Grade","value":"A"},{"category":6,"name":"Application standard","value":"ISO/IEC 15416"},{"category":6,"name":"Effective aperture","value":"n/a"},{"category":6,"name":"Lighting","value":"660"},{"category":6,"name":"Software product and version","value":"rosepetal-barcode-sdk 0.2.0 (schema 1.0)"}]}