Node barcode-verifier
Second node of @rosepetal/node-red-contrib-barcode-reader (since 1.5.2): ISO/IEC 15416 print-quality verification and GS1 symbol-specification checks of 1D barcodes, through the Rosepetal barcode SDK engine (rp-barcode serve of rosepetal-barcode-sdk). It was developed as the separate package @rosepetal/node-red-contrib-barcode-verifier 0.1.0 (never published) and imported here with its history.
Requirements
- The package's own requirements (readme). The verifier needs none of the C++ addon: only the engine client (
@rosepetal/barcode-engine-client, a dependency of the package) and the engine binary. - The engine binary
rp-barcode, from rosepetal-barcode-sdk 0.2.0 or later withverify(tested with 0.4.0, the one the client pins): itshellolistsverifyandanalyze. An engine built before theverifyop answersunsupported(the node reports it as an error, nothing is verified). It is found the same way as for the reader'srosepetalblock:RP_BARCODE_ENGINE, then the platform package@rosepetal/barcode-engine-linux-{x64,arm64}(private registry), thenrp-barcodeinPATH. - Without an engine every message fails with
done(err)(unavailable) carrying the install hint. The node never degrades to a partial or silent result.
Both nodes share one engine process per Node-RED runtime (acquire/release of the client): it starts with the first request and stops when the last barcode-verifier node, or barcode-reader node with a rosepetal block, closes.
The node
Category RP Utils. One input, one output. Verifies the print quality of one 1D barcode per region against ISO/IEC 15416 (grade 4.0-0.0 / A-F, the ISO parameters, quiet zones and dimensions) and, under a GS1 symbol specification table, the GS1 data and symbol structure and the table's limits. Symbologies: EAN-13, EAN-8, UPC-A, UPC-E, Code 128 / GS1-128, ITF / ITF-14, Code 39, Code 93 and Codabar (1D only: a 2D symbol is never read, and the GS1 tables that admit only 2D symbols fail every 1D one).
The engine version is logged once per node at its first use (Rosepetal engine 0.4.0+<commit> (protocol 1, pid N)). A running engine that dies is restarted 1 s later by the next request (every good hello resets the wait, so a crash loop retries every 1 s); a binary that fails to start is retried with a growing wait (1 s, 2 s, … up to 30 s). Whatever the engine writes to its stderr appears once in the Node-RED log as [rp-barcode] … warnings (one forwarder for both nodes, shared/engine-stderr.js).
Input
msg.<inputValue> (default msg.payload): the same image barcode-reader accepts, or an array of them.
| Input | Shape | Notes |
|---|---|---|
| Rosepetal bitmap | {data: Buffer, width, height, colorSpace, dtype: "uint8"} | colorSpace GRAY, RGB, BGR, RGBA, BGRA |
| Raw bitmap | {data: Buffer, width, height, channels} | 1, 3 or 4 channels (channels may be omitted when data.length tells it) |
| Encoded Buffer | PNG, JPEG, BMP | the size is read from the file header, so a normalized region works on them |
| Encoded Buffer | TIFF | the engine decodes it, but the node does not read its size: only a px region or autoLocate; a normalized region on a TIFF is an error |
| Array | [image, image, …] | one array of results per image, in the same order, verified in parallel ([] for an empty array) |
EXIF orientation is ignored (as in the SDK): a region refers to the stored pixels. data.length must equal width × height × channels; anything else is an error before anything is sent.
Region
msg.<regionValue> (default msg.region) = {x, y, w, h, orientation?}, the box holding the symbol with its quiet zones, or an array of them: one result per region, in order, verified in parallel (an empty array is an error). With regionValue empty no region is read: every message is auto-located (below), whatever it carries.
regionUnitsnormalized(default): the four numbers are 0-1 over the image (print-profiler'srect);px: image pixels.wandhare extents along the image axes whatever the orientation: they are never swapped at 90/270.orientation(0, 90, 180 or 270: the clockwise rotation of the printed content, i.e. the reading direction) is the region's own when present, else the node'sorientationoption.- Errors, all before anything is sent: any other key (an
angle, for instance), a normalized region outside 0-1, a region smaller than 2 px or outside the image after conversion (apxregion is rounded edge by edge and refused when it passes the image; nothing is clipped, enlarged or moved), a normalized region on an encoded image whose size cannot be read (send a bitmap, a PNG/JPEG/BMP, or usepx). Apxregion on a TIFF is checked by the engine instead (invalid_inputwhen outside). - With an array input the message's region (or
autoLocate) applies to every image: withpxunits and images of different sizes, a region outside one of them fails the whole message. autoLocate(default on): when the message carries no region,decode(normal effort, rows and columns, restricted tosymbologywhen it is notauto) locates the symbols and each symbol found is verified in its own region (the one decode read it from, else the box of its corners widened by 10 %), in decode's order. When nothing is found the whole image is verified, so the output is a singlenot_detectedresult, never an empty array. A region that is present is always used: the location runs only when the region is missing. WithautoLocateoff a missing region is an error (msg.region not found). The node sendsdecodeonlyeffort,horizontal/verticalandsymbologies, never the keys added after SDK 0.2.0 (edges,projection, the projection retries), so the engine's defaults apply: since SDK 0.3.0 that includes theedges: autoretries of the rows that read nothing (the symbols a 0.2.0 engine found come out identical and first; new ones after them), and the projection search stays off.
Options
The editor writes numbers as numbers and checkboxes as booleans; a flow edited by hand may carry numeric strings. The editor folds edition, quietZoneRule, summary, scans, denseScans, wavelengthNm, apertureMils, analyze and timeoutMs under Advanced. A key missing from the flow JSON takes its default; a value outside its vocabulary is a config error on every message until it is fixed (edition: "2020", failUnder: 5, scans: 0, spec: "gs1:42" is refused by the engine instead: unknown GS1 table).
| Option | Values | Default | Meaning |
|---|---|---|---|
name | string | barcode-verifier | node name; also the key under msg.performance |
inputValue | msg path | payload | where the image is read from |
outputValue | msg path | verification | where the result is written |
regionValue | msg path or empty | region | where the region is read from. Empty: no region is read, every message is auto-located (needs autoLocate on, else a config error); a missing key takes the default |
regionUnits | normalized | px | normalized | units of the region |
orientation | 0 | 90 | 180 | 270 | 0 | reading orientation used when the region carries none |
autoLocate | boolean | true | without a region, locate with decode and verify each symbol (a single not_detected when none is found) |
symbology | auto | EAN13 | EAN8 | UPCA | UPCE | Code128 | ITF | Code39 | Code93 | Codabar | auto | the symbology expected; a named one makes any other symbol not_detected (symbology mismatch) |
edition | 2016 | 2025 | 2016 | edition of ISO/IEC 15416 applied |
spec | iso | gs1:<table> | iso | iso: the ISO grade alone. gs1:<table> (GS1 General Specifications §5.12.3: 1, 1.8200, 2, 3, 4, 5, 6, 7.1, 7.2, 7.3, 7.4, 8, 9, 10, 11, 12.1, 12.2, 12.3, 13): also the table's allowed symbologies, X dimension, bar height, quiet zone and minimum grade limits and the GS1 data structure. 1.8200, 7.1-7.4 and 12.1 admit only 2D symbols and fail every 1D one. The X and height limits need a scale |
failUnder / warnUnder | 0-4 | 1.5 / 2.5 | verdict thresholds on quality.overall.grade: fail under failUnder, warn under warnUnder, else pass. The editor asks for a warnUnder above failUnder, or 0 for no warnings (and refuses 0 / 0); in a hand-edited flow one at or under failUnder never warns. Both 0 means the SDK defaults 1.5 / 2.5; a single 0 is kept, and a failUnder of 0 never fails on the grade (the GS1 table minimum, structure errors and a grade that cannot be computed still fail). For "no grading" turn on readOnly (Measure only) |
readOnly | boolean | false | Measure only in the editor: the V275 "0/0" mode: decode and measure, the grade does not decide the verdict. The GS1 structure, bar height and X-dimension rules still apply; the table's minimum grade does not |
heightErrors / xDimErrors / structureErrors | fail | warn | ignore | fail | GS1 policies (editor rows shown under a gs1: spec; hidden rows keep their saved values). A bar height or X dimension outside the table's limits fails (grade changed to F, the LVS Limit Action), warns or is ignored; a symbol or data structure error (element strings, check digits, linters, the 48-character limit of a GS1-128, the N ratio of an ITF-14, a symbology the table does not allow) follows structureErrors, also under iso for a symbol read in the GS1 form |
enforceAssociations | boolean | false | a missing AI association is a data structure error |
quietZoneRule | 2009 | 2000 | 2009 | ISO/IEC 15420:2009 measured minimums or ISO/IEC 15420:2000 nominal widths |
summary | worst | mean | worst | grade of each graded parameter (symbolContrast, modulation, defects, decodability): the worst scan's grade (GS1 guideline) or the grade of the mean value. The overall grade is always the mean of the scan grades |
scans | integer 1-100 | 10 | ISO scan lines across the bar height |
denseScans | boolean | true | an extra pass with one scan per row (bar height and line count); off is faster |
scale | none | pxPerMm | milsPerPixel | none | physical scale of the image. none: dimensions.evaluated is false, no mm, no GS1 X or height limits |
scaleValue / scaleValueType | number or msg path / num | msg | "" / num | the scale as a number, or a msg path (e.g. pxPerMm, template.image.pxPerMm). pxPerMm is converted to mils per pixel = 1000 / (25.4 · px/mm); the engine refuses more than 1000 mils per pixel (a pxPerMm under about 0.039) as invalid_input. A msg path that is missing or not a positive number is an error naming the path |
calibration | none | linear | card | none | reflectance calibration: linear = the grey levels of 0 % and 100 % reflectance, card = the same plus the card's Rmax and contrast |
calibBlack / calibWhite | 0-255 | 0 / 255 | grey levels of 0 % and 100 % reflectance (calibWhite above calibBlack) |
cardRmax / cardContrast | 0-100 (%) | 0 / 0 | the values printed on the reflectance card (card only); a calibration the engine cannot apply is reported in warnings |
wavelengthNm | integer 0-999 | 660 | measurement wavelength (the ISO reference and the value of every 1D GS1 table row) |
apertureMils | 0-1000 | 0 | measuring aperture; 0 = automatic (the ISO reference aperture by X dimension, or the GS1 table's) |
analyze | boolean | false | adds scans (one object per ISO scan line) to the output |
timeoutMs | integer 1-2147483647 | 10000 | timeout of each engine request (the location under autoLocate and each verification) |
Output
msg.<outputValue> (default msg.verification): the SDK's VerifyResult (result schema 1.0) as it is, plus reportText and three fields inside symbol. No key of the schema is removed or renamed. Always an array: one result per region or per located symbol (one element for a single region); under an array input, one such array per image. One result is msg.verification[0].
status:pass,warn,failornot_detected;reasons(why the verdict:overall grade 1.2 below fail threshold 1.5 (limiting: Quiet zone),no decode, …);warnings(notes that do not change the verdict).symbol:symbology,identifier,value,orientation,corners(px, TL, TR, BR, BL),region(px, the region verified),lines,confidence,checksum, plus, added by this node:formatin the reader's spelling (EAN-13,UPC-A,Code128, …),cornersNormalizedandregionNormalized(0-1 over the image;nullwhen the image size is unknown).symbolisnullundernot_detected.quality:overall.grade(0.0-4.0) andoverall.letter(A-F),limiting(the parameter that limits the grade,""at 4.0), and the ISO parameters (symbolContrast,modulation,defects,decodability,decode,edgeDetermination,minReflectance,minEdgeContrast,quietZone,inconsistent,scanGrades). How the grade is built (ISO/IEC 15416): each scan line (scans, 10 by default) gets the grade of its worst parameter, listed inscanGrades;overall.gradeis the mean of the scan grades (a scan that does not decode counts as 0), truncated to one decimal. Each graded parameter has avaluein its own unit and thegradethatsummarypicks:symbolContrastin % (Rmax − Rmin, A from 70),modulationas a ratio 0-1 (ECmin / SC, A from 0.70),defectsas a ratio 0-1 (lower is better, A up to 0.15),decodabilityas a ratio 0-1 (A from 0.62).dimensions:evaluatedistrueonly with a scale; the pixel fields (xDimPx,barHeightPx,quietZoneLeftX,quietZoneRightX,ratio, …) always, the physical ones (xDimMils,xDimMm,barHeightMm,magnification) only when evaluated.gs1:table,structure,dataStructure,errors(CODE: message),hri,elements,limits(the table's checks, each with itsstatus);nullundernot_detectedand, underiso, for a symbol that carries no GS1 data.application(the specification applied, the table's title, aperture, wavelength, edition, calibration),version(sdk,schema,dictionary),timing(the engine's own milliseconds),report(the LVS report lines{category, name, value}),reportText(the same lines asname: valuejoined by\n) and, withanalyze,scans.msg.performance[<name>].milliseconds: the time of the whole message (nameis the node name, orbarcode-verifier).
What the GS1-128 fixture of the tests (test/fixtures/gs1-128-t2.png, spec gs1:2, scale pxPerMm 8) produces, trimmed to the keys most flows read, normalized values rounded to 4 decimals (the one element of msg.verification):
{
"status": "pass",
"reasons": [],
"warnings": [],
"symbol": {
"symbology": "Code128", "identifier": "]C1", "gs1": true,
"value": "0104006381333931\u001d10ABC123",
"orientation": 0, "lines": 260, "confidence": 1, "checksum": "valid",
"corners": [{ "x": 44, "y": 4 }, { "x": 976, "y": 4 }, { "x": 976, "y": 264 }, { "x": 44, "y": 264 }],
"region": { "x": 4, "y": 4, "w": 1012, "h": 260, "orientation": 0 },
"format": "Code128",
"cornersNormalized": [{ "x": 0.0431, "y": 0.0149 }, { "x": 0.9569, "y": 0.0149 }, { "x": 0.9569, "y": 0.9851 }, { "x": 0.0431, "y": 0.9851 }],
"regionNormalized": { "x": 0.0039, "y": 0.0149, "w": 0.9922, "h": 0.9701, "orientation": 0 }
},
"quality": {
"overall": { "grade": 4, "letter": "A", "worst": 0, "mean": 0, "hasValue": true },
"limiting": "",
"symbolContrast": { "value": 80, "grade": 4, "letter": "A", "worst": 4, "mean": 80, "hasValue": true },
"modulation": { "value": 1, "grade": 4, "letter": "A" }, "defects": { "value": 0, "grade": 4, "letter": "A" },
"decodability": { "value": 1, "grade": 4, "letter": "A" }, "decode": { "pass": true, "value": 260 },
"quietZone": { "leftX": 10, "rightX": 10, "requiredLeftX": 10, "requiredRightX": 10, "pass": true },
"scanGrades": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4]
},
"dimensions": { "evaluated": true, "xDimPx": 4, "xDimMils": 19.685, "xDimMm": 0.5, "barHeightPx": 260, "barHeightMm": 32.5, "quietZoneLeftX": 10, "quietZoneRightX": 10 },
"gs1": {
"table": "2", "structure": "pass", "dataStructure": "pass", "errors": [],
"hri": "(01)04006381333931(10)ABC123",
"elements": [{ "ai": "01", "value": "04006381333931", "title": "GTIN" }, { "ai": "10", "value": "ABC123", "title": "BATCH/LOT" }],
"limits": [
{ "name": "XDim", "measured": 0.5, "min": 0.495, "max": 1.016, "unit": "mm", "status": "pass", "message": "Xdim 0.500 mm, allowed 0.495-1.016 mm (-2 %/+2 %)" },
{ "name": "Height", "measured": 32.5, "min": 31.75, "max": 0, "unit": "mm", "status": "pass", "message": "bar height 32.50 mm, minimum 31.75 mm at Xdim 0.500 mm (-5 %)" }
]
},
"application": { "spec": { "kind": "gs1_table", "table": "2" }, "tableTitle": "Table 2 - Trade Items Scanned in General Distribution Only", "apertureRef": 10, "apertureMils": 10, "wavelengthNm": 660, "edition": "2016", "calibration": "none" },
"version": { "sdk": "0.4.0", "schema": "1.0", "dictionary": "2026-01-27" },
"timing": { "totalMs": 14.674, "measureMs": 11.439, "decodeMs": 2.524, "gradeMs": 0.019, "gs1Ms": 0.023 },
"report": [{ "category": 0, "name": "Overall grade", "value": "4.0/10/660" }, { "category": 2, "name": "Symbology", "value": "GS1-128" }],
"reportText": "Overall grade: 4.0/10/660\nSymbology: GS1-128\nDecoded text: 0104006381333931<GS>10ABC123\n…"
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
(… marks keys and lines left out here; limits has seven entries for table 2, report 28 lines.)
Status
<ms> ms · pass (green), <ms> ms · warn (yellow), <ms> ms · fail and <ms> ms · not_detected (red). With several results: <ms> ms · N symbols · <worst> (the worst status of the results); under an array input: <ms> ms · N images · <worst>, grey 0 images for an empty array. A failed message shows a red ring error: <code> (the engine's error code, config for a configuration outside its vocabulary, plain error for the node's own checks).
not_detected is a result
A region with no readable symbol, or a symbol of another symbology than the one expected, is not an error: the message is sent with status not_detected, the reason (no decode, symbology mismatch: found …, …), symbolnull, gs1 null and the report, and the status turns red. Downstream logic decides what a non-read means.
Errors
Every failure calls done(err): wire a Catch node. The message is not sent, there is no partial or silent result, and the next message is processed normally (an engine that went away is restarted by a later request). The code of an EngineError is the status text after error::
| Cause | err.code |
|---|---|
engine binary not found or not executable (the message ends with the install hint: RP_BARCODE_ENGINE or the platform package); a request inside the 1 s wait after a crash (the message ends with It is restarted by a later request instead) | unavailable |
| a binary too old or too new for this node (another protocol, a bad frame) | protocol |
an engine without verify/analyze (SDK before 0.2.0 with the op) | unsupported |
the engine refused the image, the region (outside a TIFF it decoded, …), an option (unknown GS1 table "42") or the acquisition (a scale over 1000 mils per pixel) | invalid_input |
timeoutMs elapsed (the late answer is discarded) | timeout |
| the engine died with the request in flight (a later message restarts it) | exited |
| the engine's or the client's queue is full (128 requests in flight, or the engine not reading its input) | overloaded |
| the engine failed inside the verification (a recovered panic: the worker is replaced and the next message is served) | internal |
| a configuration value outside its vocabulary in the flow JSON: reported on every message until it is fixed | config (not an EngineError) |
the node's own checks (plain Error, status error): msg.<inputValue> missing or not an image, region missing without autoLocate, a region with a bad shape, unit or size, a normalized region on an image whose size is unknown, a msg scale that is missing or not a positive number | — |
A message still in flight when the node closes (a redeploy: an image or region still being verified, or a request the close itself cuts once the 2 s drain runs out, which the engine reports as exited) is dropped with a debug line (the node closed while this message was in flight) and done(): nothing is sent, nothing reaches a Catch node, and the engine is not restarted for it (the reader's rule). A reply that lands inside the drain completes its message normally.
print-profiler F4 mapping
A node-red-contrib-print-profiler template region of kind barcode1d maps to this node as follows (a barcode2d region, qualitySpec ISO15415 and the 2D-only tables have no 1D symbol to verify):
| print-profiler template | barcode-verifier |
|---|---|
rect {x, y, w, h} (normalized over the golden) and the region's orientation | msg.<regionValue> = {x, y, w, h, orientation} with regionUnits normalized (the node's orientation applies when the region carries none) |
params.qualitySpec ISO15416 | spec iso |
params.qualitySpec GS1_T<n> | spec gs1:<n> (GS1_T2 → gs1:2, GS1_T1_8200 → gs1:1.8200, GS1_T12_1 → gs1:12.1) |
params.grading.failUnder / warnUnder | failUnder / warnUnder, the same values; print-profiler's 0 / 0 (no grading) → readOnly on, never 0 / 0 here (the SDK turns both 0 into 1.5 / 2.5) |
params.gs1.structure / barHeight / xDimension | structureErrors / heightErrors / xDimErrors |
params.symbology | symbology: auto for automatic and upc_ean (the four EAN/UPC symbologies), else the named one (code128 → Code128, itf → ITF, …); msi, pdf and databar* have no equivalent: the engine does not read them |
template.image.pxPerMm | scale pxPerMm with scaleValue of type msg = the path that carries it (template.image.pxPerMm when the flow carries the template in msg.template, or pxPerMm); without it the dimensions are not evaluated |
| the result used for matching and grading | status, quality.overall.grade / quality.overall.letter, quality.limiting, gs1.structure / gs1.dataStructure / gs1.errors / gs1.limits, symbol.value (and symbol.format), reportText for the report view |
A flow of four nodes (import it in the editor): an inject carrying a template with one barcode1d region under table 2 (msg.payload must already carry the image: a camera node, a file in node with a PNG Buffer, …), a function building msg.region from rect + orientation (the scale stays in msg.template.image.pxPerMm, read by the node), the verifier with spec gs1:2 and the scale from the message, and a debug. The keys left out of the verifier node take their defaults.
[
{ "id": "f4-inject", "type": "inject", "name": "template", "props": [
{ "p": "template", "vt": "json",
"v": "{\"image\":{\"pxPerMm\":8},\"regions\":[{\"id\":\"r_1\",\"kind\":\"barcode1d\",\"name\":\"Verify1D-1\",\"rect\":{\"x\":0.0039,\"y\":0.0149,\"w\":0.9922,\"h\":0.9701},\"orientation\":0,\"params\":{\"symbology\":\"code128\",\"qualitySpec\":\"GS1_T2\",\"grading\":{\"failUnder\":1.5,\"warnUnder\":2.5},\"gs1\":{\"structure\":\"fail\",\"barHeight\":\"fail\",\"xDimension\":\"fail\"}}}]}" }
], "repeat": "", "crontab": "", "once": false, "topic": "", "x": 130, "y": 100, "wires": [["f4-map"]] },
{ "id": "f4-map", "type": "function", "name": "rect + orientation -> msg.region",
"func": "// msg.payload: the image (a Rosepetal bitmap, a raw bitmap or a PNG/JPEG/BMP Buffer)\nconst r = msg.template.regions[0];\nmsg.region = { x: r.rect.x, y: r.rect.y, w: r.rect.w, h: r.rect.h, orientation: r.orientation };\n// the node reads msg.template.image.pxPerMm itself (scaleValue of type msg)\nreturn msg;",
"outputs": 1, "x": 340, "y": 100, "wires": [["f4-verify"]] },
{ "id": "f4-verify", "type": "barcode-verifier", "name": "verifier",
"inputValue": "payload", "outputValue": "verification", "regionValue": "region", "regionUnits": "normalized",
"orientation": 0, "autoLocate": false, "symbology": "Code128", "edition": "2016", "spec": "gs1:2",
"failUnder": 1.5, "warnUnder": 2.5, "readOnly": false,
"heightErrors": "fail", "xDimErrors": "fail", "structureErrors": "fail",
"scale": "pxPerMm", "scaleValue": "template.image.pxPerMm", "scaleValueType": "msg", "timeoutMs": 10000,
"x": 580, "y": 100, "wires": [["f4-debug"]] },
{ "id": "f4-debug", "type": "debug", "name": "verification", "active": true, "tosidebar": true, "console": false,
"complete": "verification", "targetType": "msg", "x": 800, "y": 100, "wires": [] }
]2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Timing
One verification of 10 scans plus the dense pass costs about 8 ms in the SDK (Verify10Scans 7.7 ms on the development host, docs/benchmarks/2026-09-24-phase1.md of the SDK), plus under 1 ms of service overhead (make serve-budget); the engine's own split is in the result's timing, the whole message in msg.performance. denseScans off shortens it (no bar height and line count); autoLocate adds a normal-effort decode of the whole image, rows and columns (tens to hundreds of ms on a large image; the edges: auto retries of SDK 0.3.0 and later raise its cost on images where rows read nothing); analyze adds the per-scan profiles to the message (hundreds of KB with many scans). The first request after a (re)deploy also pays the engine start.
Development
The verifier's tests live in test/verifier/ and run with the package's npm test (setup in the readme): without RP_BARCODE_ENGINE the 4 end-to-end tests skip, with it all 37 run.
Three blocks of tests, none of which leaves an engine process behind (the two files that start one, node.test.js and e2e.test.js, prove it in their after):
test/verifier/options.test.js,output.test.js,editor.test.js: the configuration →verifyOptions/acquisition/ region-in-pixels mapping, the output contract and the editor (defaults equal to the runtime's, validators, help). No engine.test/verifier/node.test.js: the node undernode-red-node-test-helperon the fake engine of the client (rp.FAKE_ENGINE,test/fake-engine.jsshipped in the client package, protocol 1 in Node): normalized regions, arrays,not_detected,autoLocatewith 0 and 2 symbols, everydone(err)path, engine absent, timeout, crash,acquire/releaseand a close with a message in flight. Its knobs are the child's environment:FAKE_VERIFY_STATUS=pass|fail|not_detected(the cannedVerifyResultanswered),FAKE_DECODE_SYMBOLS=n(copies of the fixed EAN-13 adecodeanswers,0= none),FAKE_OPS=a,b,c(the ops the hello announces),FAKE_DELAY_MS,FAKE_CRASH_AFTER, and the rest documented at the top of the fake. What the fake does not do: no vocabulary validation (edition: "2020"andgs1:42pass it, the real engine refuses them); a TIFF has no size for it (0×0: no bounds check); canned answers (analyzereturns 3 scans whateverscanssays,scans: []underfail/not_detected); nowarnstatus;decodereturns N copies of the same EAN-13;shutdownexits without answering delayed requests.test/verifier/e2e.test.js: the end-to-end contract with the realrp-barcode serve, run only withRP_BARCODE_ENGINE=/path/to/rp-barcode(npm test, orRP_BARCODE_ENGINE=… node --test test/verifier/e2e.test.jsfor this file alone); it skips without the variable or when it points at the fake (ahellothat greets as+fake), fails loudly when the binary lacksverify/analyze, and starts the binary once per fixture. It compares every fixture oftest/verifier/fixtures/index.jsonthrough the node with a normalized region and the fixture's config; then the rotated EAN-13 (orientation 90 passes, 0 isnot_detected),autoLocateon the two-symbol composite (decode order, located regions), and the engine's owninvalid_inputthroughdone(err)(a px region past a TIFF, an unknown GS1 table) with the engine still serving afterwards.
test/verifier/fixtures/ holds the synthetic renders (rp-barcode synth: the ISO series contrast, defects-spots, quietzone, mustnot, a rotated EAN-13, a GS1-128, a two-symbol composite; about 6.5 KB of PNG) and index.json, their contract:
{generatedWith: {engine, command}, fixtures: [...]}; each fixture{name, file, width, height, region {x, y, w, h, orientation} (px), config (node config keys), cli (the equivalentverify/analyzeflags), expected {status, grade, letter, limiting, value, scans?}}.two-symbolshasregion: null,expected: nullandlocated[] {value, orientation, region, expected {status, grade, letter, value}}(the autoLocate order is only tested by the e2e).gs1-128-t1shares its PNG withgs1-128-t2(the same GS1-128 under table 1 fails:SYMBOLOGY_NOT_ALLOWED);contrast-0-analyzewithcontrast-0.- The expected values are those of the binary that generated them (
generatedWith.engine). A difference against a newer engine is an SDK change, not a node bug: regenerate and review the diff. - Regeneration (never by
npm test):RP_BARCODE=/path/to/rp-barcode node scripts/make-verifier-fixtures.js(dev dependencypngjs; byte-identical with the same binary, seed 1). Build the binary withmake buildin the SDK checkout so that it carries the commit (rp-barcode versionends inbuild <commit>, the hello'sengineVersionis0.4.0+<commit>; ago buildinside a checkout stamps the commit from git too, and a build with no known revision, such as one from agit archivewithout the-X main.commitflag above, saysbuild dev/0.4.0+dev), or note the SDK commit next to the command. The committed index was generated with SDKv0.4.0(build 1201c6f); against the0.2.0build of684fe76that preceded it, the PNGs are byte-identical and onlygeneratedWith.enginemoved.
CI: the package's .github/workflows/test.yml runs them with the reader's, the end-to-end ones with the engine package of the client's exact pin when the GCP_SA_KEY secret exists. The node is published with the package (build-prebuild.yml, tag v*).