You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
491 lines
15 KiB
JavaScript
491 lines
15 KiB
JavaScript
import {merge, memoize} from 'lodash';
|
|
|
|
import ImageWrapper from './common/image_wrapper';
|
|
import createLocator, {checkImageConstraints} from './locator/barcode_locator';
|
|
import BarcodeDecoder from './decoder/barcode_decoder';
|
|
import createEventedElement from './common/events';
|
|
import {release, aquire, releaseAll} from './common/buffers';
|
|
import Config from './config/config';
|
|
import {getViewport} from './common/utils';
|
|
import {Scope} from './input/SourceScope';
|
|
|
|
const vec2 = {
|
|
clone: require('gl-vec2/clone')
|
|
};
|
|
|
|
const getDecoder = memoize(decoderConfig => {
|
|
return BarcodeDecoder.create(decoderConfig);
|
|
}, decoderConfig => {
|
|
return JSON.stringify(decoderConfig);
|
|
});
|
|
|
|
const _checkImageConstraints = memoize((opts) => {
|
|
return checkImageConstraints(opts);
|
|
}, (opts) => {
|
|
return JSON.stringify(opts);
|
|
});
|
|
|
|
function createScanner(pixelCapturer) {
|
|
var _stopped = true,
|
|
_canvasContainer = {
|
|
ctx: {
|
|
image: null
|
|
},
|
|
dom: {
|
|
image: null
|
|
}
|
|
},
|
|
_inputImageWrapper,
|
|
_boxSize,
|
|
_workerPool = [],
|
|
_onUIThread = true,
|
|
_resultCollector,
|
|
_config = {},
|
|
_events = createEventedElement(),
|
|
_locator;
|
|
|
|
const source = pixelCapturer ? pixelCapturer.getSource() : {};
|
|
|
|
function updateViewportStyle(target) {
|
|
const $drawable = source.getDrawable();
|
|
const $viewport = getViewport(target);
|
|
|
|
if (!$viewport) {
|
|
return;
|
|
}
|
|
|
|
const {viewport} = source.getDimensions();
|
|
const zoom = Math.floor((((2 * viewport.x) + viewport.width) / viewport.width) * 100) / 100;
|
|
const videoWidth = zoom * viewport.width;
|
|
const translate = ((viewport.x / videoWidth) * (-100)).toFixed(5);
|
|
|
|
$drawable.style.width = `${zoom * 100}%`;
|
|
$drawable.style.transform = `translate(${translate}%, ${translate}%)`;
|
|
$drawable.style.position = 'absolute';
|
|
$viewport.style.paddingBottom = `${(viewport.height * 100 / viewport.width).toFixed(5)}%`;
|
|
$viewport.style.overflow = "hidden";
|
|
$viewport.style.height = 0;
|
|
}
|
|
|
|
function setup({numOfWorkers, target}) {
|
|
if (source.type === 'IMAGE') {
|
|
numOfWorkers = numOfWorkers >= 1 ? 1 : 0;
|
|
}
|
|
return adjustWorkerPool(numOfWorkers)
|
|
.then(() => {
|
|
if (numOfWorkers === 0) {
|
|
initBuffers();
|
|
}
|
|
})
|
|
.then(updateViewportStyle.bind(null, target));
|
|
}
|
|
|
|
function initBuffers(imageWrapper) {
|
|
if (imageWrapper) {
|
|
_inputImageWrapper = imageWrapper;
|
|
} else {
|
|
const captureSize = pixelCapturer.getCaptureSize();
|
|
_inputImageWrapper = new ImageWrapper({
|
|
x: captureSize.width,
|
|
y: captureSize.height,
|
|
});
|
|
}
|
|
if (ENV.development) {
|
|
console.log(_inputImageWrapper.size);
|
|
}
|
|
_boxSize = [
|
|
vec2.clone([0, 0]),
|
|
vec2.clone([0, _inputImageWrapper.size.y]),
|
|
vec2.clone([_inputImageWrapper.size.x, _inputImageWrapper.size.y]),
|
|
vec2.clone([_inputImageWrapper.size.x, 0])
|
|
];
|
|
_locator = createLocator(_inputImageWrapper, _config.locator);
|
|
}
|
|
|
|
function getBoundingBoxes() {
|
|
if (_config.locate) {
|
|
return _locator.locate();
|
|
} else {
|
|
return [[
|
|
vec2.clone(_boxSize[0]),
|
|
vec2.clone(_boxSize[1]),
|
|
vec2.clone(_boxSize[2]),
|
|
vec2.clone(_boxSize[3])]];
|
|
}
|
|
}
|
|
|
|
function transformResult(result, dimensions = {}) {
|
|
const {clipping = {x: 0, y: 0}} = dimensions;
|
|
let xOffset = clipping.x,
|
|
yOffset = clipping.y,
|
|
i;
|
|
|
|
if (xOffset === 0 && yOffset === 0) {
|
|
return;
|
|
}
|
|
|
|
if (result.barcodes) {
|
|
for (i = 0; i < result.barcodes.length; i++) {
|
|
transformResult(result.barcodes[i], dimensions);
|
|
}
|
|
}
|
|
|
|
if (result.line && result.line.length === 2) {
|
|
moveLine(result.line);
|
|
}
|
|
|
|
if (result.box) {
|
|
moveBox(result.box);
|
|
}
|
|
|
|
if (result.boxes && result.boxes.length > 0) {
|
|
for (i = 0; i < result.boxes.length; i++) {
|
|
moveBox(result.boxes[i]);
|
|
}
|
|
}
|
|
|
|
function moveBox(box) {
|
|
var corner = box.length;
|
|
|
|
while (corner--) {
|
|
box[corner][0] += xOffset;
|
|
box[corner][1] += yOffset;
|
|
}
|
|
}
|
|
|
|
function moveLine(line) {
|
|
line[0].x += xOffset;
|
|
line[0].y += yOffset;
|
|
line[1].x += xOffset;
|
|
line[1].y += yOffset;
|
|
}
|
|
}
|
|
|
|
function addResult (result, imageData) {
|
|
if (!imageData || !_resultCollector) {
|
|
return;
|
|
}
|
|
|
|
if (result.barcodes) {
|
|
result.barcodes.filter(barcode => barcode.codeResult)
|
|
.forEach(barcode => addResult(barcode, imageData));
|
|
} else if (result.codeResult) {
|
|
_resultCollector.addResult(imageData, source.getDimensions().canvas, result.codeResult);
|
|
}
|
|
}
|
|
|
|
function hasCodeResult (result) {
|
|
return result && (result.barcodes ?
|
|
result.barcodes.some(barcode => barcode.codeResult) :
|
|
result.codeResult);
|
|
}
|
|
|
|
function publishResult(result, imageData, bitmap) {
|
|
let resultToPublish = result;
|
|
|
|
if (result && _onUIThread) {
|
|
transformResult(result, bitmap.dimensions);
|
|
addResult(result, imageData);
|
|
resultToPublish = result.barcodes || result;
|
|
}
|
|
|
|
_events.publish("processed", resultToPublish);
|
|
if (hasCodeResult(result)) {
|
|
_events.publish("detected", resultToPublish);
|
|
}
|
|
}
|
|
|
|
function locateAndDecode(bitmap) {
|
|
var result,
|
|
boxes;
|
|
|
|
boxes = getBoundingBoxes();
|
|
if (boxes) {
|
|
result = getDecoder(_config.decoder)
|
|
.decodeFromBoundingBoxes(_inputImageWrapper, boxes);
|
|
result = result || {};
|
|
result.boxes = boxes;
|
|
publishResult(result, _inputImageWrapper.data, bitmap);
|
|
} else {
|
|
publishResult(undefined, undefined, bitmap);
|
|
}
|
|
}
|
|
|
|
function calculateClipping(canvasSize) {
|
|
const area = _config.detector.area;
|
|
const patchSize = _config.locator.patchSize || "medium";
|
|
const halfSample = _config.locator.halfSample || true;
|
|
|
|
return _checkImageConstraints({area, patchSize, canvasSize, halfSample});
|
|
}
|
|
|
|
function update() {
|
|
var availableWorker;
|
|
|
|
if (_onUIThread) {
|
|
if (_workerPool.length > 0) {
|
|
availableWorker = _workerPool.filter(function(workerThread) {
|
|
return !workerThread.busy;
|
|
})[0];
|
|
if (!availableWorker) {
|
|
return Promise.resolve();
|
|
}
|
|
}
|
|
return pixelCapturer.grabFrameData({clipping: calculateClipping})
|
|
.then((bitmap) => {
|
|
if (bitmap) {
|
|
if (availableWorker) {
|
|
availableWorker.imageData = bitmap.data;
|
|
availableWorker.dimensions = bitmap.dimensions;
|
|
availableWorker.busy = true;
|
|
availableWorker.worker.postMessage({
|
|
cmd: 'process',
|
|
imageData: availableWorker.imageData
|
|
}, [availableWorker.imageData.buffer]);
|
|
} else {
|
|
_inputImageWrapper.data = bitmap.data;
|
|
locateAndDecode(bitmap);
|
|
}
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
});
|
|
}
|
|
|
|
return Promise.resolve(locateAndDecode());
|
|
}
|
|
|
|
function startContinuousUpdate() {
|
|
var next = null,
|
|
delay = 1000 / (_config.frequency === 0 ? 10 : (_config.frequency || 10));
|
|
|
|
_stopped = false;
|
|
(function frame(timestamp) {
|
|
next = next || timestamp;
|
|
if (!_stopped) {
|
|
if (timestamp >= next) {
|
|
next += delay;
|
|
update().then(() => {
|
|
window.requestAnimFrame(frame);
|
|
});
|
|
} else {
|
|
window.requestAnimFrame(frame);
|
|
}
|
|
}
|
|
}(performance.now()));
|
|
}
|
|
|
|
function start() {
|
|
if (_onUIThread && source.type === "CAMERA") {
|
|
startContinuousUpdate();
|
|
} else {
|
|
update();
|
|
}
|
|
}
|
|
|
|
function initWorker(cb) {
|
|
const captureSize = pixelCapturer.getCaptureSize();
|
|
const workerThread = {
|
|
worker: undefined,
|
|
imageData: new Uint8Array(aquire(captureSize.width * captureSize.height)),
|
|
busy: true
|
|
};
|
|
|
|
const blobURL = generateWorkerBlob();
|
|
workerThread.worker = new Worker(blobURL);
|
|
|
|
workerThread.worker.onmessage = function(e) {
|
|
if (e.data.event === 'initialized') {
|
|
URL.revokeObjectURL(blobURL);
|
|
workerThread.busy = false;
|
|
release(e.data.imageData);
|
|
if (ENV.development) {
|
|
console.log("Worker initialized");
|
|
}
|
|
return cb(workerThread);
|
|
} else if (e.data.event === 'processed') {
|
|
release(e.data.imageData);
|
|
workerThread.busy = false;
|
|
publishResult(e.data.result, workerThread.imageData, workerThread.dimensions);
|
|
} else if (e.data.event === 'error') {
|
|
if (ENV.development) {
|
|
console.log("Worker error: " + e.data.message);
|
|
}
|
|
}
|
|
};
|
|
|
|
workerThread.worker.postMessage({
|
|
cmd: 'init',
|
|
size: {x: captureSize.width, y: captureSize.height},
|
|
imageData: workerThread.imageData,
|
|
config: configForWorker(_config)
|
|
}, [workerThread.imageData.buffer]);
|
|
}
|
|
|
|
function configForWorker(config) {
|
|
return Object.assign({}, config, {
|
|
inputStream: Object.assign({}, config.inputStream, {target: null})
|
|
});
|
|
}
|
|
|
|
function workerInterface(factory) {
|
|
/* eslint-disable no-undef*/
|
|
if (factory) {
|
|
var Quagga = factory().default;
|
|
if (!Quagga) {
|
|
self.postMessage({'event': 'error', message: 'Quagga could not be created'});
|
|
return;
|
|
}
|
|
}
|
|
var imageWrapper,
|
|
scanner = Quagga._worker.createScanner();
|
|
|
|
self.onmessage = function(e) {
|
|
if (e.data.cmd === 'init') {
|
|
var config = e.data.config;
|
|
config.numOfWorkers = 0;
|
|
imageWrapper = new Quagga.ImageWrapper({
|
|
x: e.data.size.x,
|
|
y: e.data.size.y
|
|
}, new Uint8Array(e.data.imageData));
|
|
scanner.init(config, imageWrapper).then(ready);
|
|
scanner.subscribe("processed", onProcessed);
|
|
} else if (e.data.cmd === 'process') {
|
|
imageWrapper.data = new Uint8Array(e.data.imageData);
|
|
scanner.start();
|
|
}
|
|
};
|
|
|
|
function onProcessed(result) {
|
|
self.postMessage({
|
|
'event': 'processed',
|
|
imageData: imageWrapper.data,
|
|
result: result
|
|
}, [imageWrapper.data.buffer]);
|
|
}
|
|
|
|
function ready() { // eslint-disable-line
|
|
self.postMessage({'event': 'initialized', imageData: imageWrapper.data}, [imageWrapper.data.buffer]);
|
|
}
|
|
|
|
/* eslint-enable */
|
|
}
|
|
|
|
function generateWorkerBlob() {
|
|
var blob,
|
|
factorySource;
|
|
|
|
/* jshint ignore:start */
|
|
if (typeof __factorySource__ !== 'undefined') {
|
|
factorySource = __factorySource__; // eslint-disable-line no-undef
|
|
}
|
|
/* jshint ignore:end */
|
|
|
|
blob = new Blob(['(' + workerInterface.toString() + ')(' + factorySource + ');'],
|
|
{type: 'text/javascript'});
|
|
|
|
return window.URL.createObjectURL(blob);
|
|
}
|
|
|
|
function adjustWorkerPool(capacity) {
|
|
return new Promise((resolve) => {
|
|
const increaseBy = capacity - _workerPool.length;
|
|
if (increaseBy === 0) {
|
|
return resolve();
|
|
}
|
|
if (increaseBy < 0) {
|
|
const workersToTerminate = _workerPool.slice(increaseBy);
|
|
workersToTerminate.forEach(function(workerThread) {
|
|
workerThread.worker.terminate();
|
|
if (ENV.development) {
|
|
console.log("Worker terminated!");
|
|
}
|
|
});
|
|
_workerPool = _workerPool.slice(0, increaseBy);
|
|
return resolve();
|
|
} else {
|
|
for (var i = 0; i < increaseBy; i++) {
|
|
initWorker(workerInitialized);
|
|
}
|
|
|
|
function workerInitialized(workerThread) {
|
|
_workerPool.push(workerThread);
|
|
if (_workerPool.length >= capacity){
|
|
resolve();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
return {
|
|
init: function(config, imageWrapper) {
|
|
_stopped = true;
|
|
_config = merge({}, Config, config);
|
|
|
|
if (imageWrapper) {
|
|
_onUIThread = false;
|
|
initBuffers(imageWrapper);
|
|
}
|
|
return Promise.resolve();
|
|
},
|
|
start: function() {
|
|
if (_onUIThread) {
|
|
adjustWorkerPool(0);
|
|
return source.waitUntilReady()
|
|
.then(setup.bind(null, _config))
|
|
.then(start);
|
|
}
|
|
start();
|
|
},
|
|
isRunning: function() {
|
|
return !_stopped;
|
|
},
|
|
stop: function() {
|
|
_stopped = true;
|
|
adjustWorkerPool(0);
|
|
releaseAll();
|
|
if (source.getScope() === Scope.INTERNAL) {
|
|
source.stop();
|
|
}
|
|
_events.publish("stopped");
|
|
},
|
|
applyConfig(newConfig) {
|
|
return this.init(newConfig);
|
|
},
|
|
pause: function() {
|
|
_stopped = true;
|
|
},
|
|
subscribe(eventName, callback, once = false) {
|
|
if (!once) {
|
|
return _events.subscribe(eventName, callback);
|
|
}
|
|
_events.once(eventName, callback);
|
|
},
|
|
unsubscribe(eventName, callback) {
|
|
_events.unsubscribe(eventName, callback);
|
|
},
|
|
registerResultCollector: function(resultCollector) {
|
|
if (resultCollector && typeof resultCollector.addResult === 'function') {
|
|
_resultCollector = resultCollector;
|
|
}
|
|
},
|
|
decodeSingle() {
|
|
return new Promise((resolve, reject) => {
|
|
_events.once("processed", (result) => {
|
|
this.stop();
|
|
if (result && result.codeResult && result.codeResult.code) {
|
|
return resolve(result);
|
|
}
|
|
return reject(result);
|
|
}, true);
|
|
this.start();
|
|
});
|
|
},
|
|
canvas: _canvasContainer
|
|
};
|
|
}
|
|
|
|
export default createScanner;
|