Skip to content

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 with verify (tested with 0.4.0, the one the client pins): its hello lists verify and analyze. An engine built before the verify op answers unsupported (the node reports it as an error, nothing is verified). It is found the same way as for the reader's rosepetal block: RP_BARCODE_ENGINE, then the platform package @rosepetal/barcode-engine-linux-{x64,arm64} (private registry), then rp-barcode in PATH.
  • 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.

InputShapeNotes
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 BufferPNG, JPEG, BMPthe size is read from the file header, so a normalized region works on them
Encoded BufferTIFFthe 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.

  • regionUnits normalized (default): the four numbers are 0-1 over the image (print-profiler's rect); px: image pixels. w and h are 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's orientation option.
  • 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 (a px region 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 use px). A px region on a TIFF is checked by the engine instead (invalid_input when outside).
  • With an array input the message's region (or autoLocate) applies to every image: with px units 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 to symbology when it is not auto) 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 single not_detected result, never an empty array. A region that is present is always used: the location runs only when the region is missing. With autoLocate off a missing region is an error (msg.region not found). The node sends decode only effort, horizontal/vertical and symbologies, 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 the edges: auto retries 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).

OptionValuesDefaultMeaning
namestringbarcode-verifiernode name; also the key under msg.performance
inputValuemsg pathpayloadwhere the image is read from
outputValuemsg pathverificationwhere the result is written
regionValuemsg path or emptyregionwhere 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
regionUnitsnormalized | pxnormalizedunits of the region
orientation0 | 90 | 180 | 2700reading orientation used when the region carries none
autoLocatebooleantruewithout a region, locate with decode and verify each symbol (a single not_detected when none is found)
symbologyauto | EAN13 | EAN8 | UPCA | UPCE | Code128 | ITF | Code39 | Code93 | Codabarautothe symbology expected; a named one makes any other symbol not_detected (symbology mismatch)
edition2016 | 20252016edition of ISO/IEC 15416 applied
speciso | gs1:<table>isoiso: 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 / warnUnder0-41.5 / 2.5verdict 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)
readOnlybooleanfalseMeasure 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 / structureErrorsfail | warn | ignorefailGS1 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
enforceAssociationsbooleanfalsea missing AI association is a data structure error
quietZoneRule2009 | 20002009ISO/IEC 15420:2009 measured minimums or ISO/IEC 15420:2000 nominal widths
summaryworst | meanworstgrade 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
scansinteger 1-10010ISO scan lines across the bar height
denseScansbooleantruean extra pass with one scan per row (bar height and line count); off is faster
scalenone | pxPerMm | milsPerPixelnonephysical scale of the image. none: dimensions.evaluated is false, no mm, no GS1 X or height limits
scaleValue / scaleValueTypenumber or msg path / num | msg"" / numthe 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
calibrationnone | linear | cardnonereflectance calibration: linear = the grey levels of 0 % and 100 % reflectance, card = the same plus the card's Rmax and contrast
calibBlack / calibWhite0-2550 / 255grey levels of 0 % and 100 % reflectance (calibWhite above calibBlack)
cardRmax / cardContrast0-100 (%)0 / 0the values printed on the reflectance card (card only); a calibration the engine cannot apply is reported in warnings
wavelengthNminteger 0-999660measurement wavelength (the ISO reference and the value of every 1D GS1 table row)
apertureMils0-10000measuring aperture; 0 = automatic (the ISO reference aperture by X dimension, or the GS1 table's)
analyzebooleanfalseadds scans (one object per ISO scan line) to the output
timeoutMsinteger 1-214748364710000timeout 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, fail or not_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: format in the reader's spelling (EAN-13, UPC-A, Code128, …), cornersNormalized and regionNormalized (0-1 over the image; null when the image size is unknown). symbol is null under not_detected.
  • quality: overall.grade (0.0-4.0) and overall.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 in scanGrades; overall.grade is the mean of the scan grades (a scan that does not decode counts as 0), truncated to one decimal. Each graded parameter has a value in its own unit and the grade that summary picks: symbolContrast in % (Rmax − Rmin, A from 70), modulation as a ratio 0-1 (ECmin / SC, A from 0.70), defects as a ratio 0-1 (lower is better, A up to 0.15), decodability as a ratio 0-1 (A from 0.62).
  • dimensions: evaluated is true only 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 its status); null under not_detected and, under iso, 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 as name: value joined by \n) and, with analyze, scans.
  • msg.performance[<name>].milliseconds: the time of the whole message (name is the node name, or barcode-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):

json
{
  "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…"
}

(… 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::

Causeerr.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 fixedconfig (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.

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 templatebarcode-verifier
rect {x, y, w, h} (normalized over the golden) and the region's orientationmsg.<regionValue> = {x, y, w, h, orientation} with regionUnits normalized (the node's orientation applies when the region carries none)
params.qualitySpec ISO15416spec 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 / warnUnderfailUnder / 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 / xDimensionstructureErrors / heightErrors / xDimErrors
params.symbologysymbology: 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.pxPerMmscale 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 gradingstatus, 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.

json
[
  { "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": [] }
]

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 under node-red-node-test-helper on the fake engine of the client (rp.FAKE_ENGINE, test/fake-engine.js shipped in the client package, protocol 1 in Node): normalized regions, arrays, not_detected, autoLocate with 0 and 2 symbols, every done(err) path, engine absent, timeout, crash, acquire/release and a close with a message in flight. Its knobs are the child's environment: FAKE_VERIFY_STATUS=pass|fail|not_detected (the canned VerifyResult answered), FAKE_DECODE_SYMBOLS=n (copies of the fixed EAN-13 a decode answers, 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" and gs1:42 pass it, the real engine refuses them); a TIFF has no size for it (0×0: no bounds check); canned answers (analyze returns 3 scans whatever scans says, scans: [] under fail/not_detected); no warn status; decode returns N copies of the same EAN-13; shutdown exits without answering delayed requests.
  • test/verifier/e2e.test.js: the end-to-end contract with the real rp-barcode serve, run only with RP_BARCODE_ENGINE=/path/to/rp-barcode (npm test, or RP_BARCODE_ENGINE=… node --test test/verifier/e2e.test.js for this file alone); it skips without the variable or when it points at the fake (a hello that greets as +fake), fails loudly when the binary lacks verify/analyze, and starts the binary once per fixture. It compares every fixture of test/verifier/fixtures/index.json through the node with a normalized region and the fixture's config; then the rotated EAN-13 (orientation 90 passes, 0 is not_detected), autoLocate on the two-symbol composite (decode order, located regions), and the engine's own invalid_input through done(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 equivalent verify/analyze flags), expected {status, grade, letter, limiting, value, scans?}}.
  • two-symbols has region: null, expected: null and located[] {value, orientation, region, expected {status, grade, letter, value}} (the autoLocate order is only tested by the e2e).
  • gs1-128-t1 shares its PNG with gs1-128-t2 (the same GS1-128 under table 1 fails: SYMBOLOGY_NOT_ALLOWED); contrast-0-analyze with contrast-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 dependency pngjs; byte-identical with the same binary, seed 1). Build the binary with make build in the SDK checkout so that it carries the commit (rp-barcode version ends in build <commit>, the hello's engineVersion is 0.4.0+<commit>; a go build inside a checkout stamps the commit from git too, and a build with no known revision, such as one from a git archive without the -X main.commit flag above, says build dev / 0.4.0+dev), or note the SDK commit next to the command. The committed index was generated with SDK v0.4.0 (build 1201c6f); against the 0.2.0 build of 684fe76 that preceded it, the PNGs are byte-identical and only generatedWith.engine moved.

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*).