Skip to content

Commit a4e76a7

Browse files
authored
Merge pull request #655 from ericblade/dev
A lot of pharmacode updates
2 parents ea327ba + f8bc245 commit a4e76a7

16 files changed

Lines changed: 933 additions & 148 deletions

File tree

.github/copilot-instructions.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,31 @@ This file controls which packages `npm-check-updates` can upgrade:
141141

142142
## Testing Requirements
143143

144+
### CRITICAL: NODE_OPTIONS Flag
145+
146+
**⚠️ IMPORTANT:** Cypress tests MUST be run with `NODE_OPTIONS=--openssl-legacy-provider` due to Node.js 17+ security changes disabling legacy OpenSSL algorithms. Our build dependencies require these algorithms.
147+
148+
**Always use npm scripts** (which include this flag automatically):
149+
```bash
150+
npm run cypress:run # Correct - includes NODE_OPTIONS
151+
npm run cypress:open # Correct - interactive runner with NODE_OPTIONS
152+
npm run test # Correct - full test suite with NODE_OPTIONS
153+
```
154+
155+
**When running Cypress directly, include the flag:**
156+
```bash
157+
# Correct - runs specific spec with NODE_OPTIONS
158+
npm run cypress:run -- --spec cypress/e2e/integration.cy.ts
159+
160+
# Also correct - explicit NODE_OPTIONS
161+
npx cross-env NODE_ENV=development BUILD_ENV=development NODE_OPTIONS=--openssl-legacy-provider npx cypress run --spec cypress/e2e/integration.cy.ts
162+
163+
# WRONG - missing NODE_OPTIONS, will timeout
164+
npx cypress run --spec cypress/e2e/integration.cy.ts
165+
```
166+
167+
Tests will appear to hang with "timed out waiting for async callback" if `NODE_OPTIONS` is missing.
168+
144169
### Running Tests
145170

146171
- **Type checking**: `npm run check-types`

cypress/e2e/integration.cy.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
window.ENV = { development: true, production: false, node: false };
22

3-
import '../../test/integration/decoders/ean.spec.ts';
4-
import '../../test/integration/decoders/ean_extended.spec.ts';
5-
import '../../test/integration/decoders/ean_supplement_format.spec.ts';
3+
// Integration tests that aren't specific decoders
4+
import '../../test/integration/external-reader.spec.ts';
5+
import '../../test/integration/integration.spec.ts';
6+
import '../../test/integration/reader-order.spec.ts';
7+
8+
// Decoders
9+
import '../../test/integration/decoders/2of5.spec.ts';
10+
import '../../test/integration/decoders/codabar.spec.ts';
611
import '../../test/integration/decoders/code_128.spec.ts';
7-
import '../../test/integration/decoders/code_39.spec.ts';
8-
import '../../test/integration/decoders/code_39_vin.spec.ts';
912
import '../../test/integration/decoders/code_32.spec.ts';
13+
import '../../test/integration/decoders/code_39_vin.spec.ts';
14+
import '../../test/integration/decoders/code_39.spec.ts';
15+
import '../../test/integration/decoders/code_93.spec.ts';
1016
import '../../test/integration/decoders/ean_8.spec.ts';
11-
import '../../test/integration/decoders/upc.spec.ts';
12-
import '../../test/integration/decoders/upc_e.spec.ts';
13-
import '../../test/integration/decoders/codabar.spec.ts';
17+
import '../../test/integration/decoders/ean_extended.spec.ts';
18+
import '../../test/integration/decoders/ean_supplement_format.spec.ts';
19+
import '../../test/integration/decoders/ean.spec.ts';
1420
import '../../test/integration/decoders/i2of5.spec.ts';
15-
import '../../test/integration/decoders/2of5.spec.ts';
16-
import '../../test/integration/decoders/code_93.spec.ts';
17-
import '../../test/integration/external-reader.spec.ts';
18-
import '../../test/integration/integration.spec.ts';
19-
import '../../test/integration/reader-order.spec.ts';
21+
import '../../test/integration/decoders/upc_e.spec.ts';
22+
import '../../test/integration/decoders/upc.spec.ts';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import '../../../../test/integration/decoders/pharmacode.spec.ts';

docs/examples/file_input.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ <h3>Working with file-input</h3>
8686
<span>Single Channel</span>
8787
<input type="checkbox" name="input-stream_single-channel" />
8888
</label>
89+
<label>
90+
<span>Locate (barcode finder)</span>
91+
<input type="checkbox" checked="checked" name="locate" />
92+
</label>
8993
</fieldset>
9094
</div>
9195
<div id="result_strip">

docs/examples/file_input.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
$(function() {
22
var App = {
33
init: function() {
4+
App.syncCheckboxesFromState();
45
App.attachListeners();
56
},
7+
syncCheckboxesFromState: function() {
8+
var self = this;
9+
$(".controls .reader-config-group input[type=checkbox]").each(function() {
10+
var $checkbox = $(this),
11+
name = $checkbox.attr("name"),
12+
state = self._convertNameToState(name),
13+
value = self._accessByPath(self.state, state);
14+
$checkbox.prop("checked", !!value);
15+
});
16+
},
617
attachListeners: function() {
718
var self = this;
819

@@ -147,6 +158,7 @@ $(function() {
147158
}
148159

149160
Quagga.onProcessed(function(result) {
161+
console.warn('* onProcessed', result);
150162
var drawingCtx = Quagga.canvas.ctx.overlay,
151163
drawingCanvas = Quagga.canvas.dom.overlay,
152164
area;
@@ -174,10 +186,11 @@ $(function() {
174186
drawingCtx.strokeStyle = "#0F0";
175187
drawingCtx.strokeRect(area.x, area.y, area.width, area.height);
176188
}
177-
}
189+
}
178190
});
179191

180192
Quagga.onDetected(function(result) {
193+
console.warn('* onDetected', result);
181194
var code = result.codeResult.code,
182195
$node,
183196
canvas = Quagga.canvas.dom.image;

src/common/cv_utils.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -542,13 +542,25 @@ export function computeGray(imageData, outArray, config) {
542542

543543
if (singleChannel) {
544544
for (let i = 0; i < l; i++) {
545-
// eslint-disable-next-line no-param-reassign
546-
outArray[i] = imageData[i * 4 + 0];
545+
const alpha = imageData[i * 4 + 3];
546+
if (alpha === 0) {
547+
// eslint-disable-next-line no-param-reassign
548+
outArray[i] = 255; // Treat transparent pixels as white
549+
} else {
550+
// eslint-disable-next-line no-param-reassign
551+
outArray[i] = imageData[i * 4 + 0];
552+
}
547553
}
548554
} else {
549555
for (let i = 0; i < l; i++) {
550-
// eslint-disable-next-line no-param-reassign
551-
outArray[i] = 0.299 * imageData[i * 4 + 0] + 0.587 * imageData[i * 4 + 1] + 0.114 * imageData[i * 4 + 2];
556+
const alpha = imageData[i * 4 + 3];
557+
if (alpha === 0) {
558+
// eslint-disable-next-line no-param-reassign
559+
outArray[i] = 255; // Treat transparent pixels as white
560+
} else {
561+
// eslint-disable-next-line no-param-reassign
562+
outArray[i] = 0.299 * imageData[i * 4 + 0] + 0.587 * imageData[i * 4 + 1] + 0.114 * imageData[i * 4 + 2];
563+
}
552564
}
553565
}
554566
}

src/common/test/cv_utils.spec.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,6 +1049,87 @@ describe('CV Utils', () => {
10491049
computeGray(imageData, outArray);
10501050
expect(outArray[0]).to.equal(0);
10511051
});
1052+
1053+
it('should treat transparent pixels as white regardless of RGB values', () => {
1054+
// Transparent black (0,0,0,0) should be treated as white
1055+
const imageData = new Uint8Array([
1056+
0, 0, 0, 0, // transparent black
1057+
]);
1058+
const outArray = new Uint8Array(1);
1059+
computeGray(imageData, outArray);
1060+
// Result should be 255 (white) because alpha=0
1061+
expect(outArray[0]).to.equal(255);
1062+
// Alpha channel should NOT be modified (imageData not used after computeGray)
1063+
expect(imageData[3]).to.equal(0);
1064+
});
1065+
1066+
it('should handle transparent white as white', () => {
1067+
// Transparent white (255,255,255,0) should still be white
1068+
const imageData = new Uint8Array([
1069+
255, 255, 255, 0, // transparent white
1070+
]);
1071+
const outArray = new Uint8Array(1);
1072+
computeGray(imageData, outArray);
1073+
// Result should be 255 (white)
1074+
expect(outArray[0]).to.equal(255);
1075+
// Alpha channel should NOT be modified
1076+
expect(imageData[3]).to.equal(0);
1077+
});
1078+
1079+
it('should handle semi-transparent pixels by their RGB value', () => {
1080+
// Semi-transparent red (255,0,0,128) should use RGB red luminance
1081+
const imageData = new Uint8Array([
1082+
255, 0, 0, 128, // semi-transparent red
1083+
]);
1084+
const outArray = new Uint8Array(1);
1085+
computeGray(imageData, outArray);
1086+
// Result should be red luminance (~76), alpha=128 is not fully transparent
1087+
expect(outArray[0]).to.be.closeTo(76, 1);
1088+
// Alpha channel should NOT be modified (not fully transparent)
1089+
expect(imageData[3]).to.equal(128);
1090+
});
1091+
1092+
it('should handle fully opaque pixels with various alpha values', () => {
1093+
// Same RGB but different alpha should produce same grayscale output (if not alpha=0)
1094+
const imageData = new Uint8Array([
1095+
255, 0, 0, 255, // fully opaque red
1096+
255, 0, 0, 128, // semi-transparent red
1097+
255, 0, 0, 1, // barely opaque red
1098+
]);
1099+
const outArray = new Uint8Array(3);
1100+
computeGray(imageData, outArray);
1101+
// All three should produce the same grayscale red luminance (none are alpha=0)
1102+
expect(outArray[0]).to.be.closeTo(76, 1);
1103+
expect(outArray[1]).to.be.closeTo(76, 1);
1104+
expect(outArray[2]).to.be.closeTo(76, 1);
1105+
// Alpha channels should NOT be modified
1106+
expect(imageData[3]).to.equal(255);
1107+
expect(imageData[7]).to.equal(128);
1108+
expect(imageData[11]).to.equal(1);
1109+
});
1110+
1111+
it('should handle PNG with transparent background and opaque content', () => {
1112+
// Simulating a PNG: transparent areas + opaque black text
1113+
const imageData = new Uint8Array([
1114+
0, 0, 0, 0, // transparent (background)
1115+
0, 0, 0, 255, // opaque black (text)
1116+
255, 255, 255, 0, // transparent white (background)
1117+
0, 0, 0, 255, // opaque black (text)
1118+
]);
1119+
const outArray = new Uint8Array(4);
1120+
computeGray(imageData, outArray);
1121+
// All transparent areas (alpha=0) should become white (255) in grayscale output
1122+
// All opaque areas should use their RGB values
1123+
expect(outArray[0]).to.equal(255); // transparent black -> white
1124+
expect(outArray[1]).to.equal(0); // opaque black -> black
1125+
expect(outArray[2]).to.equal(255); // transparent white -> white
1126+
expect(outArray[3]).to.equal(0); // opaque black -> black
1127+
// Alpha channels should NOT be modified (imageData not used after computeGray)
1128+
expect(imageData[3]).to.equal(0); // first pixel alpha (unchanged)
1129+
expect(imageData[7]).to.equal(255); // second pixel alpha (unchanged)
1130+
expect(imageData[11]).to.equal(0); // third pixel alpha (unchanged)
1131+
expect(imageData[15]).to.equal(255); // fourth pixel alpha (unchanged)
1132+
});
10521133
});
10531134

10541135
describe('grayAndHalfSampleFromCanvasData', () => {

src/decoder/barcode_decoder.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,79 @@ export default {
235235
}];
236236
}
237237

238+
/**
239+
* Validate that barcode position is stable across adjacent Y-scanlines.
240+
* Real barcodes have consistent start position; tilted barcodes shift left/right as Y changes.
241+
* @param {Array} line The original scan line [p1, p2]
242+
* @param {Object} result The successful decode result with .start position
243+
* @param {Object} reader The reader instance that succeeded
244+
* @param {Object} inputImageWrapper The full image data
245+
* @returns {boolean} true if barcode position is stable (≥1 adjacent Y-line matches)
246+
*/
247+
function validateAdjacentYLines(line, result, reader, inputImageWrapper) {
248+
// Extract original Y position and X start position
249+
const originalY = Math.round(line[1].y);
250+
const originalXStart = result.start;
251+
const constructorFn = reader.constructor;
252+
const requiredMatches = (constructorFn && constructorFn.adjacentLineValidationMatches) || 0;
253+
254+
if (requiredMatches <= 0) {
255+
return true;
256+
}
257+
258+
let matchCount = 0;
259+
let done = false;
260+
261+
// Check Y±1, Y±2, Y±3 to see if barcode appears at same X position
262+
for (const yOffset of [1, 2, 3]) {
263+
if (done) {
264+
break;
265+
}
266+
for (const direction of [-1, 1]) {
267+
if (done) {
268+
break;
269+
}
270+
const newY = originalY + (yOffset * direction);
271+
272+
// Bounds check
273+
if (newY < 0 || newY >= inputImageWrapper.size.y) {
274+
continue;
275+
}
276+
277+
// Create new line at adjusted Y, keeping same X range
278+
const newP1 = { x: line[0].x, y: newY };
279+
const newP2 = { x: line[1].x, y: newY };
280+
281+
try {
282+
// Extract grayscale at new Y
283+
const newBarcodeLine = Bresenham.getBarcodeLine(inputImageWrapper, newP1, newP2);
284+
285+
// Binarize
286+
Bresenham.toBinaryLine(newBarcodeLine);
287+
288+
// Set the row for _findStart() to search in
289+
reader._row = newBarcodeLine.line;
290+
291+
// Try to find barcode start at this Y
292+
const startFound = reader._findStart();
293+
294+
if (startFound !== null && startFound.start === originalXStart) {
295+
matchCount++;
296+
if (matchCount >= requiredMatches) {
297+
done = true;
298+
break;
299+
}
300+
}
301+
} catch (e) {
302+
// Ignore errors, treat failures as "no match" so we can try again on the next line to try
303+
}
304+
}
305+
}
306+
307+
const isValid = matchCount >= requiredMatches;
308+
return isValid;
309+
}
310+
238311
/**
239312
* Attempts to decode a barcode from a scan line.
240313
* Readers are tried in order (as specified in config.readers).
@@ -261,12 +334,33 @@ export default {
261334
}
262335

263336
// Iterate readers in order - first successful decode wins
337+
let successfulReaderIndex = -1;
264338
for (i = 0; i < _barcodeReaders.length && result === null; i++) {
339+
// Provide image context to readers that want it (e.g., pharmacode PGM dumps)
340+
if (typeof _barcodeReaders[i].setImageWrapper === 'function') {
341+
_barcodeReaders[i].setImageWrapper(inputImageWrapper);
342+
}
265343
result = _barcodeReaders[i].decodePattern(barcodeLine.line);
344+
if (result !== null) {
345+
successfulReaderIndex = i;
346+
}
266347
}
267348
if (result === null) {
268349
return null;
269350
}
351+
352+
// Validate that barcode position is stable across adjacent Y-scanlines
353+
// This rejects tilted barcodes that only appear valid at one specific angle
354+
// Only apply to PharmacodeReader (which explicitly made _findStart public for this validation)
355+
if (successfulReaderIndex >= 0 && _barcodeReaders[successfulReaderIndex].constructor.name === 'PharmacodeReader') {
356+
if (!validateAdjacentYLines(line, result, _barcodeReaders[successfulReaderIndex], inputImageWrapper)) {
357+
if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'test') {
358+
// console.log(`[DEBUG] REJECTED by adjacent Y-line validation: barcode position unstable (tilted barcode)`);
359+
}
360+
return null;
361+
}
362+
}
363+
270364
return {
271365
codeResult: result,
272366
barcodeLine,

src/quagga.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ const QuaggaJSStaticInterface = {
153153
return new Promise((resolve, reject) => {
154154
try {
155155
this.init(config, () => {
156+
// Sync the decodeSingle instance's canvas to the global _context so Quagga.canvas works
157+
_context.canvasContainer = quaggaInstance.context.canvasContainer;
158+
156159
Events.once('processed', (result) => {
157160
quaggaInstance.stop();
158161
if (resultCallback) {

src/reader/barcode_reader.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export interface Barcode {
4747
end: number,
4848
endInfo?: BarcodePosition,
4949
format: BarcodeFormat,
50+
pattern?: string,
5051
start: number,
5152
startInfo: BarcodePosition,
5253
supplement?: Barcode,
@@ -77,6 +78,9 @@ export abstract class BarcodeReader {
7778
};
7879
}
7980

81+
// Reader-specific adjacent-line validation threshold (0 => disabled)
82+
static adjacentLineValidationMatches = 0;
83+
8084
constructor(config: BarcodeReaderConfig, supplements?: Array<BarcodeReader>) {
8185
this._row = [];
8286
this.config = config || {};

0 commit comments

Comments
 (0)