pull/385/merge
Alexei Domratchev 6 years ago committed by GitHub
commit 9bec06a30d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

BIN
.DS_Store vendored

Binary file not shown.

@ -1,33 +1,31 @@
{
"presets": [
"@babel/env",
"@babel/typescript"
],
"plugins": [
"check-es2015-constants",
"transform-es2015-arrow-functions",
"transform-es2015-block-scoped-functions",
"transform-es2015-block-scoping",
["transform-es2015-classes", { "loose": true }],
["transform-es2015-computed-properties", { "loose": true }],
["transform-es2015-destructuring", { "loose": true }],
["transform-es2015-for-of", { "loose": true }],
"transform-es2015-function-name",
"transform-es2015-literals",
"transform-es2015-object-super",
"transform-es2015-parameters",
"transform-es2015-shorthand-properties",
["transform-es2015-spread", { "loose": true }],
"transform-es2015-sticky-regex",
["transform-es2015-template-literals", { "loose": true }],
"transform-es2015-unicode-regex",
"transform-es2015-typeof-symbol",
"transform-object-rest-spread",
"lodash"
"@babel/proposal-class-properties",
"@babel/proposal-object-rest-spread",
"@babel/transform-runtime"
],
"env": {
"commonjs": {
"node": {
"plugins": [
["transform-es2015-modules-commonjs", { "loose": true }],
"add-module-exports"
]
},
"test": {
"plugins": [
"add-module-exports",
[
"istanbul",
{
"exclude": [
"**/*.spec.js"
]
}
]
]
}
}
}

@ -1,35 +1,21 @@
{
"settings" : {
"ecmascript": 6,
"jsx": true
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"env": {
"browser": true,
"es6": true,
"node": true
},
"ecmaFeatures": {
"blockBindings": true,
"forOf": true,
"defaultParams": true,
"globalReturn": false,
"modules": true,
"objectLiteralShorthandMethods": true,
"objectLiteralShorthandProperties": true,
"templateStrings": true,
"spread": true,
"jsx": true,
"arrowFunctions": true,
"classes": true,
"destructuring": true,
"objectLiteralComputedProperties": true
},
"globals": {
"ENV": true,
"beforeEach": true,
"describe": true,
"it": true,
"expect": true,
"sinon": true
"ENV": "readonly",
"beforeEach": "readonly",
"describe": "readonly",
"it": "readonly",
"expect": "readonly",
"sinon": "readonly"
},
"rules": {
"no-unused-expressions": 1,
@ -42,7 +28,10 @@
"eqeqeq": 2,
"guard-for-in": 2,
"wrap-iife": 0,
"no-use-before-define": [1, "nofunc"],
"no-use-before-define": [
1,
"nofunc"
],
"new-cap": 2,
"quotes": 0,
"strict": 0,
@ -52,18 +41,44 @@
"no-plusplus": 0,
"no-unused-vars": 1,
"no-trailing-spaces": 2,
// STYLE
"max-params": [2, 7],
"key-spacing": [1, {
beforeColon: false,
afterColon: true
}],
"indent": [2, 4],
"brace-style": [2, "1tbs"],
"comma-spacing": [2, {before: false, after: true}],
"comma-style": [2, "last"],
"consistent-this": [1, "self"],
"max-params": [
2,
7
],
"key-spacing": [
1,
{
"beforeColon": false,
"afterColon": true
}
],
"indent": [
"error",
4,
{
"SwitchCase": 1
}
],
"brace-style": [
2,
"1tbs"
],
"comma-spacing": [
2,
{
"before": false,
"after": true
}
],
"comma-style": [
2,
"last"
],
"consistent-this": [
1,
"self"
],
"eol-last": 0,
"new-parens": 2,
"no-array-constructor": 2,
@ -74,11 +89,19 @@
"no-spaced-func": 1,
"no-shadow": 2,
"no-undef": 2,
"padded-blocks": [2, "never"],
"semi": [2, "always"],
"space-after-keywords": [2, "always"],
"padded-blocks": [
2,
"never"
],
"semi": [
2,
"always"
],
"space-infix-ops": 2,
"max-len" : [1, 120],
"max-len": [
1,
120
],
"consistent-return": 2,
"yoda": 2
}

@ -1,21 +0,0 @@
module.exports = function(grunt) {
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
karma: {
unit: {
configFile: 'karma.conf.js'
},
integration: {
configFile: 'karma-integration.conf.js'
}
}
});
grunt.loadNpmTasks('grunt-karma');
grunt.loadTasks('tasks');
grunt.registerTask('test', ['karma']);
grunt.registerTask('integrationtest', ['karma:integration']);
};

@ -95,8 +95,7 @@ The above condition evaluates to:
## <a name="installing">Installing</a>
QuaggaJS can be installed using __npm__, __bower__, or by including it with
the __script__ tag.
QuaggaJS can be installed using __npm__ or by including it with the __script__ tag.
### NPM
@ -115,14 +114,6 @@ Currently, the full functionality is only available through the browser. When
using QuaggaJS within __node__, only file-based decoding is available. See the
example for [node_examples](#node-example).
### Bower
You can also install QuaggaJS through __bower__:
```console
> bower install quagga
```
### Script-Tag Anno 1998
You can simply include `dist/quagga.min.js` in your project and you are ready
@ -181,19 +172,19 @@ node.
```javascript
Quagga.init({
inputStream : {
name : "Live",
type : "LiveStream",
name : 'Live',
type : 'LiveStream',
target: document.querySelector('#yourElement') // Or '#yourElement' (optional)
},
decoder : {
readers : ["code_128_reader"]
readers : ['code_128_reader']
}
}, function (err) {
if (err) {
console.log(err);
return
}
console.log("Initialization finished. Ready to start");
console.log('Initialization finished. Ready to start');
Quagga.start();
});
```
@ -475,7 +466,7 @@ supplements you have to provide them in the configuration as followed:
```javascript
decoder: {
readers: [{
format: "ean_reader",
format: 'ean_reader',
config: {
supplements: [
'ean_5_reader', 'ean_2_reader'
@ -508,7 +499,7 @@ Only two properties are relevant for the use in Quagga (`halfSample` and
```javascript
{
halfSample: true,
patchSize: "medium", // x-small, small, medium, large, x-large
patchSize: 'medium', // x-small, small, medium, large, x-large
debug: {
showCanvas: false,
showPatches: false,
@ -552,43 +543,43 @@ locating-mechanism for more robust results.
```javascript
Quagga.decodeSingle({
decoder: {
readers: ["code_128_reader"] // List of active readers
readers: ['code_128_reader'] // List of active readers
},
locate: true, // try to locate the barcode in the image
src: '/test/fixtures/code_128/image-001.jpg' // or 'data:image/jpg;base64,' + data
}, function (result) {
if (result.codeResult) {
console.log("result", result.codeResult.code);
console.log('result', result.codeResult.code);
} else {
console.log("not detected");
console.log('not detected');
}
});
```
### <a name="node-example">Using node</a>
The following example illustrates the use of QuaggaJS within a node
environment. It's almost identical to the browser version with the difference
that node does not support web-workers out of the box. Therefore the config
The following example illustrates the use of QuaggaJS within a node environment.
It's almost identical to the browser version with the difference
that node does not support web workers out of the box. Therefore the config
property `numOfWorkers` must be explicitly set to `0`.
```javascript
var Quagga = require('quagga').default;
Quagga.decodeSingle({
src: "image-abc-123.jpg",
src: 'image-abc-123.jpg',
numOfWorkers: 0, // Needs to be 0 when used within node
inputStream: {
size: 800 // restrict input-size to be 800px in width (long-side)
size: 800 // restrict input size to be 800px in width (long side)
},
decoder: {
readers: ["code_128_reader"] // List of active readers
readers: ['code_128_reader'] // List of active readers
},
}, function(result) {
if(result.codeResult) {
console.log("result", result.codeResult.code);
console.log('result', result.codeResult.code);
} else {
console.log("not detected");
console.log('not detected');
}
});
```
@ -601,11 +592,11 @@ A growing collection of tips & tricks to improve the various aspects of Quagga.
Barcodes too far away from the camera, or a lens too close to the object
result in poor recognition rates and Quagga might respond with a lot of
false-positives.
false positives.
Starting in Chrome 59 you can now make use of `capabilities` and directly
control the zoom of the camera. Head over to the
[web-cam demo](https://serratus.github.io/quaggaJS/examples/live_w_locator.html)
[web cam demo](https://serratus.github.io/quaggaJS/examples/live_w_locator.html)
and check out the __Zoom__ feature.
You can read more about those `capabilities` in
@ -618,7 +609,7 @@ recognition logic.
Since Chrome 59 you can turn on/off the __Torch__ of our device and vastly
improve the quality of the images. Head over to the
[web-cam demo](https://serratus.github.io/quaggaJS/examples/live_w_locator.html)
[web cam demo](https://serratus.github.io/quaggaJS/examples/live_w_locator.html)
and check out the __Torch__ feature.
To find out more about this feature [read on](https://www.oberhofer.co/mediastreamtrack-and-its-capabilities).
@ -626,8 +617,8 @@ To find out more about this feature [read on](https://www.oberhofer.co/mediastre
## Tests
Unit Tests can be run with [Karma][karmaUrl] and written using
[Mocha][mochaUrl], [Chai][chaiUrl] and [SinonJS][sinonUrl]. Coverage reports are
automatically generated in the coverage/ folder.
[Mocha][mochaUrl], [Chai][chaiUrl] and [SinonJS][sinonUrl].
Coverage reports are automatically generated in the coverage/ folder.
```console
> npm install
@ -639,7 +630,7 @@ In case you want to take a deeper dive into the inner workings of Quagga, get to
know the _debugging_ capabilities of the current implementation. The various
flags exposed through the `config` object give you the abilily to visualize
almost every step in the processing. Because of the introduction of the
web-workers, and their restriction not to have access to the DOM, the
web workers, and their restriction not to have access to the DOM, the
configuration must be explicitly set to `config.numOfWorkers = 0` in order to
work.
@ -652,19 +643,19 @@ bugs in the implementation.
### Creating a ``ResultCollector``
You can easily create a new ``ResultCollector`` by calling its ``create``
method with a configuration.
You can easily create a new ``ResultCollector`` by calling its constructor
with a configuration.
```javascript
var resultCollector = Quagga.ResultCollector.create({
const resultCollector = new Quagga.ResultCollector({
capture: true, // keep track of the image producing this result
capacity: 20, // maximum number of results to store
blacklist: [ // list containing codes which should not be recorded
{code: "3574660239843", format: "ean_13"}],
{code: '3574660239843', format: 'ean_13'}],
filter: function (codeResult) {
// only store results which match this constraint
// returns true/false
// e.g.: return codeResult.format === "ean_13";
// e.g.: return codeResult.format === 'ean_13';
return true;
}
});
@ -684,7 +675,7 @@ do not fit into a certain schema. Calling ``getResults`` on the
```javascript
{
codeResult: {}, // same as in onDetected event
frame: "data:image/png;base64,iVBOR..." // dataURL of the gray-scaled image
frame: 'data:image/png;base64,iVBOR...' // dataURL of the gray-scaled image
}
```
@ -705,6 +696,18 @@ on the ``singleChannel`` flag in the configuration when using ``decodeSingle``.
## <a name="changelog">Changelog</a>
### 2019-07-31
- Internal Changes
- Converted to TypeScript
- Upgraded tooling to Webpack 4.38, Babel 7.5, Karma 4.2, Chai 4.2, Sinon 7.3
- Removed redundant files
- Improvements
- Removed all 3rd party dependencies for Web version
- Changed ImageDebug API
- Changed ``Box`` interface to be ``[Point, Point, Point, Point]``
- Improved performance by utilizing ES2015+ features, bitwise operators, etc.
- Added support for ``area`` measured in pixels (px)
### 2017-06-07
- Improvements
- added `muted` and `playsinline` to `<video/>` to make it work for Safari 11
@ -759,18 +762,15 @@ Take a look at the release-notes (
[0.10.0](https://github.com/serratus/quaggaJS/releases/tag/v0.10.0))
### 2016-02-18
- Internal Changes
- Restructuring into meaningful folders
- Removing debug-code in production build
### 2016-02-15
Take a look at the release-notes (
[0.9.0](https://github.com/serratus/quaggaJS/releases/tag/v0.9.0))
### 2015-11-22
- Fixes
- Fixed inconsistencies for Code 128 decoding (See
[\#76](https://github.com/serratus/quaggaJS/issues/76))

@ -1,68 +0,0 @@
{
"name": "quagga",
"version": "0.12.1",
"description": "An advanced barcode-scanner written in JavaScript",
"main": "dist/quagga.js",
"ignore": [
"build",
"doc",
"spec",
"src",
"tasks",
"test",
".gitignore",
".npmignore",
"Gruntfile.js",
"karma-integration.conf.js",
"karma.conf.js",
"package.json",
"test-main.js"
],
"devDependencies": {
"async": "^0.9.0",
"grunt": "~0.4.5",
"grunt-contrib-jshint": "~0.10.0",
"grunt-contrib-nodeunit": "~0.4.1",
"grunt-contrib-uglify": "~0.5.0",
"grunt-karma": "^0.9.0",
"grunt-requirejs": "^0.4.2",
"karma": "latest",
"karma-chai": "latest",
"karma-chrome-launcher": "^0.1.12",
"karma-coverage": "^0.3.1",
"karma-mocha": "latest",
"karma-phantomjs-launcher": "^0.1.4",
"karma-requirejs": "^0.2.2",
"karma-sinon": "^1.0.4",
"karma-sinon-chai": "^0.2.0",
"sinon": "^1.12.1"
},
"directories": {
"doc": "doc"
},
"scripts": {
"test": "karma start"
},
"repository": {
"type": "git",
"url": "https://github.com/serratus/quaggaJS.git"
},
"bugs": {
"url": "https://github.com/serratus/quaggaJS/issues"
},
"keywords": [
"quagga",
"quaggajs",
"barcode",
"ean",
"code128",
"code39",
"codabar",
"i2of5",
"upc",
"getusermedia",
"imageprocessing"
],
"author": "Christoph Oberhofer <ch.oberhofer@gmail.com>",
"license": "MIT"
}

@ -1,2 +0,0 @@
return require('quagga');
}));

@ -1,11 +0,0 @@
(function (root, factory) {
var factorySource = factory.toString();
if (typeof module !== 'undefined') {
module.exports = factory(factorySource);
} else {
//Browser globals case. Just assign the
//result to a property on the global.
root.Quagga = factory(factorySource);
}
}(this, function (__factorySource__) {

1115
dist/quagga.d.ts vendored

File diff suppressed because it is too large Load Diff

16101
dist/quagga.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,5 +0,0 @@
module.exports = {
production: false,
development: true,
node: false
};

5
env/node.js vendored

@ -1,5 +0,0 @@
module.exports = {
production: true,
development: false,
node: true
};

5
env/production.js vendored

@ -1,5 +0,0 @@
module.exports = {
production: true,
development: false,
node: false
};

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
@ -11,22 +12,21 @@
function getUserMedia(constraints, success, failure) {
navigator.getUserMedia(constraints, function (stream) {
var videoSrc = (window.URL && window.URL.createObjectURL(stream)) || stream;
const videoSrc = (window.URL && window.URL.createObjectURL(stream)) || stream;
success.apply(null, [videoSrc]);
}, failure);
}
function initCamera(constraints, video, callback) {
getUserMedia(constraints, function (src) {
getUserMedia(constraints, src => {
video.src = src;
video.addEventListener('loadeddata', function() {
var attempts = 10;
video.addEventListener('loadeddata', () => {
let attempts = 10;
function checkVideo() {
const checkVideo = () => {
if (attempts > 0) {
if (video.videoWidth > 0 && video.videoHeight > 0) {
console.log(video.videoWidth + "px x " + video.videoHeight + "px");
console.log(`${video.videoWidth}px x ${video.videoHeight}px`);
video.play();
callback();
} else {
@ -36,11 +36,11 @@
callback('Unable to play video stream.');
}
attempts--;
}
};
checkVideo();
}, false);
}, function(e) {
}, e => {
console.log(e);
});
}
@ -53,16 +53,16 @@
}
window.addEventListener('load', function () {
var constraints = {
const constraints = {
video: {
mandatory: {
minWidth: 1280,
minHeight: 720
}
}
},
video = document.createElement('video'),
canvas = document.createElement('canvas');
};
const video = document.createElement('video'),
const canvas = document.createElement('canvas');
document.body.appendChild(video);
document.body.appendChild(canvas);
@ -73,11 +73,10 @@
copyToCanvas(video, canvas.getContext('2d'));
});
}, false);
</script>
</head>
<body>
<body>
</body>
</html>

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
@ -23,12 +24,11 @@
<section id="container" class="container">
<h3>Working with file-input</h3>
<p>This example let's you select an image from your local filesystem.
<p>This example lets you select an image from your local filesystem.
QuaggaJS then tries to decode the barcode using
the preferred method (<strong>Code128</strong> or <strong>EAN</strong>).
There is no server interaction needed as the
file is simply accessed through the <a
href="http://www.w3.org/TR/file-upload/">File API</a>.</p>
There is no server interaction needed as the file is simply accessed
through the <a href="http://www.w3.org/TR/file-upload/">File API</a>.</p>
<p>This also works great on a wide range of mobile-phones where the camera
access through <code>getUserMedia</code> is still very limited.</p>
@ -109,4 +109,5 @@
<script src="../dist/quagga.js" type="text/javascript"></script>
<script src="file_input.js" type="text/javascript"></script>
</body>
</html>

@ -6,36 +6,36 @@ $(function() {
attachListeners: function () {
var self = this;
$(".controls input[type=file]").on("change", function(e) {
if (e.target.files && e.target.files.length) {
App.decode(URL.createObjectURL(e.target.files[0]));
$('.controls input[type=file]').on('change', event => {
if (event.target.files && event.target.files.length) {
App.decode(URL.createObjectURL(event.target.files[0]));
}
});
$(".controls button").on("click", function(e) {
var input = document.querySelector(".controls input[type=file]");
$('.controls button').on('click', event => {
var input = document.querySelector('.controls input[type=file]');
if (input.files && input.files.length) {
App.decode(URL.createObjectURL(input.files[0]));
}
});
$(".controls .reader-config-group").on("change", "input, select", function(e) {
e.preventDefault();
var $target = $(e.target),
value = $target.attr("type") === "checkbox" ? $target.prop("checked") : $target.val(),
name = $target.attr("name"),
$('.controls .reader-config-group').on('change', 'input, select', event => {
event.preventDefault();
var $target = $(event.target),
value = $target.attr('type') === 'checkbox' ? $target.prop('checked') : $target.val(),
name = $target.attr('name'),
state = self._convertNameToState(name);
console.log("Value of "+ state + " changed to " + value);
console.log(`Value of ${state} changed to ${value}`);
self.setState(state, value);
});
},
_accessByPath: function (obj, path, val) {
var parts = path.split('.'),
depth = parts.length,
setter = (typeof val !== "undefined") ? true : false;
setter = (typeof val !== 'undefined') ? true : false;
return parts.reduce(function(o, key, i) {
return parts.reduce((o, key, i) => {
if (setter && (i + 1) === depth) {
o[key] = val;
}
@ -43,25 +43,25 @@ $(function() {
}, obj);
},
_convertNameToState: function (name) {
return name.replace("_", ".").split("-").reduce(function(result, value) {
return name.replace('_', '.').split('-').reduce((result, value) => {
return result + value.charAt(0).toUpperCase() + value.substring(1);
});
},
detachListeners: function () {
$(".controls input[type=file]").off("change");
$(".controls .reader-config-group").off("change", "input, select");
$(".controls button").off("click");
$('.controls input[type=file]').off('change');
$('.controls .reader-config-group').off('change', 'input, select');
$('.controls button').off('click');
},
decode: function (src) {
var self = this,
config = $.extend({}, self.state, { src: src });
Quagga.decodeSingle(config, function(result) {});
Quagga.decodeSingle(config, result => { });
},
setState: function (path, value) {
var self = this;
if (typeof self._accessByPath(self.inputMapper, path) === "function") {
if (typeof self._accessByPath(self.inputMapper, path) === 'function') {
value = self._accessByPath(self.inputMapper, path)(value);
}
@ -84,7 +84,7 @@ $(function() {
readers: function (value) {
if (value === 'ean_extended') {
return [{
format: "ean_reader",
format: 'ean_reader',
config: {
supplements: [
'ean_5_reader', 'ean_2_reader'
@ -93,7 +93,7 @@ $(function() {
}];
}
return [{
format: value + "_reader",
format: value + '_reader',
config: {}
}];
}
@ -105,12 +105,12 @@ $(function() {
singleChannel: false
},
locator: {
patchSize: "medium",
patchSize: 'medium',
halfSample: true
},
decoder: {
readers: [{
format: "code_128_reader",
format: 'code_128_reader',
config: {}
}]
},
@ -149,25 +149,25 @@ $(function() {
if (result) {
if (result.boxes) {
drawingCtx.clearRect(0, 0, parseInt(drawingCanvas.getAttribute("width")), parseInt(drawingCanvas.getAttribute("height")));
result.boxes.filter(function (box) {
return box !== result.box;
}).forEach(function (box) {
Quagga.ImageDebug.drawPath(box, {x: 0, y: 1}, drawingCtx, {color: "green", lineWidth: 2});
drawingCtx.clearRect(0, 0, parseInt(drawingCanvas.getAttribute('width')), parseInt(drawingCanvas.getAttribute('height')));
result.boxes.forEach(box => {
if (box !== result.box) {
Quagga.ImageDebug.drawPath(box, drawingCtx, 'green', 2);
}
});
}
if (result.box) {
Quagga.ImageDebug.drawPath(result.box, {x: 0, y: 1}, drawingCtx, {color: "#00F", lineWidth: 2});
Quagga.ImageDebug.drawPath(result.box, drawingCtx, '#00F', 2);
}
if (result.codeResult && result.codeResult.code) {
Quagga.ImageDebug.drawPath(result.line, {x: 'x', y: 'y'}, drawingCtx, {color: 'red', lineWidth: 3});
Quagga.ImageDebug.drawPath(result.line, drawingCtx, 'red', 3);
}
if (App.state.inputStream.area) {
area = calculateRectFromArea(drawingCanvas, App.state.inputStream.area);
drawingCtx.strokeStyle = "#0F0";
drawingCtx.strokeStyle = '#0F0';
drawingCtx.strokeRect(area.x, area.y, area.width, area.height);
}
}
@ -179,8 +179,8 @@ $(function() {
canvas = Quagga.canvas.dom.image;
$node = $('<li><div class="thumbnail"><div class="imgWrapper"><img /></div><div class="caption"><h4 class="code"></h4></div></div></li>');
$node.find("img").attr("src", canvas.toDataURL());
$node.find("h4.code").html(code);
$("#result_strip ul.thumbnails").prepend($node);
$node.find('img').attr('src', canvas.toDataURL());
$node.find('h4.code').html(code);
$('#result_strip ul.thumbnails').prepend($node);
});
});

@ -1,19 +1,19 @@
$(function () {
var resultCollector = Quagga.ResultCollector.create({
var resultCollector = new Quagga.ResultCollector({
capture: true,
capacity: 20,
blacklist: [{
code: "WIWV8ETQZ1", format: "code_93"
code: 'WIWV8ETQZ1', format: 'code_93'
}, {
code: "EH3C-%GU23RK3", format: "code_93"
code: 'EH3C-%GU23RK3', format: 'code_93'
}, {
code: "O308SIHQOXN5SA/PJ", format: "code_93"
code: 'O308SIHQOXN5SA/PJ', format: 'code_93'
}, {
code: "DG7Q$TV8JQ/EN", format: "code_93"
code: 'DG7Q$TV8JQ/EN', format: 'code_93'
}, {
code: "VOFD1DB5A.1F6QU", format: "code_93"
code: 'VOFD1DB5A.1F6QU', format: 'code_93'
}, {
code: "4SO64P4X8 U4YUU1T-", format: "code_93"
code: '4SO64P4X8 U4YUU1T-', format: 'code_93'
}],
filter: function (codeResult) {
// only store results which match this constraint
@ -67,14 +67,14 @@ $(function() {
applySettingsVisibility: function (setting, capability) {
// depending on type of capability
if (typeof capability === 'boolean') {
var node = document.querySelector('input[name="settings_' + setting + '"]');
let node = document.querySelector(`input[name="settings_${setting}"]`);
if (node) {
node.parentNode.style.display = capability ? 'block' : 'none';
}
return;
}
if (window.MediaSettingsRange && capability instanceof window.MediaSettingsRange) {
var node = document.querySelector('select[name="settings_' + setting + '"]');
let node = document.querySelector(`select[name="settings_${setting}"]`);
if (node) {
this.updateOptionsForMediaRange(node, capability);
node.parentNode.style.display = 'block';
@ -90,12 +90,12 @@ $(function() {
function pruneText(text) {
return text.length > 30 ? text.substr(0, 30) : text;
}
var $deviceSelection = document.getElementById("deviceSelection");
var $deviceSelection = document.getElementById('deviceSelection');
while ($deviceSelection.firstChild) {
$deviceSelection.removeChild($deviceSelection.firstChild);
}
devices.forEach(function(device) {
var $option = document.createElement("option");
devices.forEach(device => {
var $option = document.createElement('option');
$option.value = device.deviceId || device.id;
$option.appendChild(document.createTextNode(pruneText(device.label || device.deviceId || device.id)));
$option.selected = streamLabel === device.label;
@ -107,43 +107,43 @@ $(function() {
var self = this;
self.initCameraSelection();
$(".controls").on("click", "button.stop", function(e) {
e.preventDefault();
$('.controls').on('click', 'button.stop', event => {
event.preventDefault();
Quagga.stop();
self._printCollectedResults();
});
$(".controls .reader-config-group").on("change", "input, select", function(e) {
e.preventDefault();
var $target = $(e.target),
value = $target.attr("type") === "checkbox" ? $target.prop("checked") : $target.val(),
name = $target.attr("name"),
$('.controls .reader-config-group').on('change', 'input, select', event => {
event.preventDefault();
var $target = $(event.target),
value = $target.attr('type') === 'checkbox' ? $target.prop('checked') : $target.val(),
name = $target.attr('name'),
state = self._convertNameToState(name);
console.log("Value of "+ state + " changed to " + value);
console.log(`Value of ${state} changed to ${value}`);
self.setState(state, value);
});
},
_printCollectedResults: function () {
var results = resultCollector.getResults(),
$ul = $("#result_strip ul.collector");
$ul = $('#result_strip ul.collector');
results.forEach(function (result) {
var $li = $('<li><div class="thumbnail"><div class="imgWrapper"><img /></div><div class="caption"><h4 class="code"></h4></div></div></li>');
$li.find("img").attr("src", result.frame);
$li.find("h4.code").html(result.codeResult.code + " (" + result.codeResult.format + ")");
$li.find('img').attr('src', result.frame);
$li.find('h4.code').html(result.codeResult.code + ' (' + result.codeResult.format + ')');
$ul.prepend($li);
});
},
_accessByPath: function (obj, path, val) {
var parts = path.split('.'),
depth = parts.length,
setter = (typeof val !== "undefined") ? true : false;
setter = (typeof val !== 'undefined') ? true : false;
return parts.reduce(function (o, key, i) {
if (setter && (i + 1) === depth) {
if (typeof o[key] === "object" && typeof val === "object") {
if (typeof o[key] === 'object' && typeof val === 'object') {
Object.assign(o[key], val);
} else {
o[key] = val;
@ -153,13 +153,13 @@ $(function() {
}, obj);
},
_convertNameToState: function (name) {
return name.replace("_", ".").split("-").reduce(function(result, value) {
return name.replace('_', '.').split('-').reduce((result, value) => {
return result + value.charAt(0).toUpperCase() + value.substring(1);
});
},
detachListeners: function () {
$(".controls").off("click", "button.stop");
$(".controls .reader-config-group").off("change", "input, select");
$('.controls').off('click', 'button.stop');
$('.controls .reader-config-group').off('change', 'input, select');
},
applySetting: function (setting, value) {
var track = Quagga.CameraAccess.getActiveTrack();
@ -175,7 +175,7 @@ $(function() {
setState: function (path, value) {
var self = this;
if (typeof self._accessByPath(self.inputMapper, path) === "function") {
if (typeof self._accessByPath(self.inputMapper, path) === 'function') {
value = self._accessByPath(self.inputMapper, path)(value);
}
@ -212,7 +212,7 @@ $(function() {
readers: function (value) {
if (value === 'ean_extended') {
return [{
format: "ean_reader",
format: 'ean_reader',
config: {
supplements: [
'ean_5_reader', 'ean_2_reader'
@ -229,23 +229,23 @@ $(function() {
},
state: {
inputStream: {
type : "LiveStream",
type: 'LiveStream',
constraints: {
width: { min: 640 },
height: { min: 480 },
facingMode: "environment",
facingMode: 'environment',
aspectRatio: { min: 1, max: 2 }
}
},
locator: {
patchSize: "medium",
patchSize: 'medium',
halfSample: true
},
numOfWorkers: 2,
frequency: 10,
decoder: {
readers: [{
format: "code_128_reader",
format: 'code_128_reader',
config: {}
}]
},
@ -262,20 +262,20 @@ $(function() {
if (result) {
if (result.boxes) {
drawingCtx.clearRect(0, 0, parseInt(drawingCanvas.getAttribute("width")), parseInt(drawingCanvas.getAttribute("height")));
result.boxes.filter(function (box) {
return box !== result.box;
}).forEach(function (box) {
Quagga.ImageDebug.drawPath(box, {x: 0, y: 1}, drawingCtx, {color: "green", lineWidth: 2});
drawingCtx.clearRect(0, 0, parseInt(drawingCanvas.getAttribute('width')), parseInt(drawingCanvas.getAttribute('height')));
result.boxes.forEach(box => {
if (box !== result.box) {
Quagga.ImageDebug.drawPath(box, drawingCtx, 'green', 2);
}
});
}
if (result.box) {
Quagga.ImageDebug.drawPath(result.box, {x: 0, y: 1}, drawingCtx, {color: "#00F", lineWidth: 2});
Quagga.ImageDebug.drawPath(result.box, drawingCtx, '#00F', 2);
}
if (result.codeResult && result.codeResult.code) {
Quagga.ImageDebug.drawPath(result.line, {x: 'x', y: 'y'}, drawingCtx, {color: 'red', lineWidth: 3});
Quagga.ImageDebug.drawPath(result.line, drawingCtx, 'red', 3);
}
}
});
@ -288,9 +288,9 @@ $(function() {
var $node = null, canvas = Quagga.canvas.dom.image;
$node = $('<li><div class="thumbnail"><div class="imgWrapper"><img /></div><div class="caption"><h4 class="code"></h4></div></div></li>');
$node.find("img").attr("src", canvas.toDataURL());
$node.find("h4.code").html(code);
$("#result_strip ul.thumbnails").prepend($node);
$node.find('img').attr('src', canvas.toDataURL());
$node.find('h4.code').html(code);
$('#result_strip ul.thumbnails').prepend($node);
}
});

@ -1,4 +1,4 @@
var Quagga = require('../lib/quagga').default;
var Quagga = require('../dist/quagga.node').default;
var buffer = require('fs').readFileSync('../test/fixtures/code_128/image-001.jpg');
@ -8,20 +8,20 @@ function decode(buff){
src: buff,
numOfWorkers: 0,
inputStream: {
mime: "image/jpeg",
mime: 'image/jpeg',
size: 800,
area: {
top: "10%",
right: "5%",
left: "5%",
bottom: "10%"
top: '10%',
right: '5%',
left: '5%',
bottom: '10%'
}
}
}, function (result) {
if (result.codeResult) {
console.log("result", result.codeResult.code);
console.log('result', result.codeResult.code);
} else {
console.log("not detected");
console.log('not detected');
}
});
}

@ -1,21 +1,21 @@
var Quagga = require('../lib/quagga').default;
var Quagga = require('../dist/quagga.node').default;
Quagga.decodeSingle({
src: "../test/fixtures/code_128/image-001.jpg",
src: '../test/fixtures/code_128/image-001.jpg',
numOfWorkers: 0,
inputStream: {
size: 800,
area: {
top: "10%",
right: "5%",
left: "5%",
bottom: "10%"
top: '10%',
right: '5%',
left: '5%',
bottom: '10%'
}
}
}, function (result) {
if (result.codeResult) {
console.log("result", result.codeResult.code);
console.log('result', result.codeResult.code);
} else {
console.log("not detected");
console.log('not detected');
}
});

@ -3,31 +3,32 @@ $(function() {
init: function () {
var config = this.config[this.state.decoder.readers[0].format] || this.config.default;
config = $.extend(true, {}, config, this.state);
Quagga.init(config, function() {
Quagga.init(config, () => {
App.attachListeners();
Quagga.start();
});
},
config: {
"default": {
inputStream: { name: "Test",
type: "ImageStream",
'default': {
inputStream: {
name: 'Test',
type: 'ImageStream',
length: 10,
size: 800
},
locator: {
patchSize: "medium",
patchSize: 'medium',
halfSample: true
}
},
"i2of5_reader": {
'i2of5_reader': {
inputStream: {
size: 800,
type: "ImageStream",
type: 'ImageStream',
length: 5
},
locator: {
patchSize: "small",
patchSize: 'small',
halfSample: false
}
}
@ -35,30 +36,30 @@ $(function() {
attachListeners: function () {
var self = this;
$(".controls").on("click", "button.next", function(e) {
e.preventDefault();
$('.controls').on('click', 'button.next', event => {
event.preventDefault();
Quagga.start();
});
$(".controls .reader-config-group").on("change", "input, select", function(e) {
e.preventDefault();
var $target = $(e.target),
value = $target.attr("type") === "checkbox" ? $target.prop("checked") : $target.val(),
name = $target.attr("name"),
$('.controls .reader-config-group').on('change', 'input, select', event => {
event.preventDefault();
var $target = $(event.target),
value = $target.attr('type') === 'checkbox' ? $target.prop('checked') : $target.val(),
name = $target.attr('name'),
states = self._convertNameToStates(name);
console.log("Value of "+ states + " changed to " + value);
console.log(`Value of ${states} changed to ${value}`);
self.setState(states, value);
});
},
detachListeners: function () {
$(".controls").off("click", "button.next");
$(".controls .reader-config-group").off("change", "input, select");
$('.controls').off('click', 'button.next');
$('.controls .reader-config-group').off('change', 'input, select');
},
_accessByPath: function (obj, path, val) {
var parts = path.split('.'),
depth = parts.length,
setter = (typeof val !== "undefined") ? true : false;
setter = (typeof val !== 'undefined') ? true : false;
return parts.reduce(function (o, key, i) {
if (setter && (i + 1) === depth) {
@ -68,10 +69,10 @@ $(function() {
}, obj);
},
_convertNameToStates: function (names) {
return names.split(";").map(this._convertNameToState.bind(this));
return names.split(';').map(this._convertNameToState.bind(this));
},
_convertNameToState: function (name) {
return name.replace("_", ".").split("-").reduce(function(result, value) {
return name.replace('_', '.').split('-').reduce((result, value) => {
return result + value.charAt(0).toUpperCase() + value.substring(1);
});
},
@ -81,7 +82,7 @@ $(function() {
paths.forEach(function (path) {
var mappedValue;
if (typeof self._accessByPath(self.inputMapper, path) === "function") {
if (typeof self._accessByPath(self.inputMapper, path) === 'function') {
mappedValue = self._accessByPath(self.inputMapper, path)(value);
}
self._accessByPath(self.state, path, mappedValue);
@ -97,7 +98,7 @@ $(function() {
readers: function (value) {
if (value === 'ean_extended') {
return [{
format: "ean_reader",
format: 'ean_reader',
config: {
supplements: [
'ean_5_reader', 'ean_2_reader'
@ -106,24 +107,22 @@ $(function() {
}];
}
return [{
format: value + "_reader",
format: value + '_reader',
config: {}
}];
}
},
inputStream: {
src: function(value) {
return "../test/fixtures/" + value + "/"
}
src: value => `../test/fixtures/${value}/`
}
},
state: {
inputStream: {
src: "../test/fixtures/code_128/"
src: '../test/fixtures/code_128/'
},
decoder: {
readers: [{
format: "code_128_reader",
format: 'code_128_reader',
config: {}
}]
}
@ -139,20 +138,20 @@ $(function() {
if (result) {
if (result.boxes) {
drawingCtx.clearRect(0, 0, parseInt(drawingCanvas.getAttribute("width")), parseInt(drawingCanvas.getAttribute("height")));
result.boxes.filter(function (box) {
return box !== result.box;
}).forEach(function (box) {
Quagga.ImageDebug.drawPath(box, {x: 0, y: 1}, drawingCtx, {color: "green", lineWidth: 2});
drawingCtx.clearRect(0, 0, parseInt(drawingCanvas.getAttribute('width')), parseInt(drawingCanvas.getAttribute('height')));
result.boxes.forEach(box => {
if (box !== result.box) {
Quagga.ImageDebug.drawPath(box, drawingCtx, 'green', 2);
}
});
}
if (result.box) {
Quagga.ImageDebug.drawPath(result.box, {x: 0, y: 1}, drawingCtx, {color: "#00F", lineWidth: 2});
Quagga.ImageDebug.drawPath(result.box, drawingCtx, '#00F', 2);
}
if (result.codeResult && result.codeResult.code) {
Quagga.ImageDebug.drawPath(result.line, {x: 'x', y: 'y'}, drawingCtx, {color: 'red', lineWidth: 3});
Quagga.ImageDebug.drawPath(result.line, drawingCtx, 'red', 3);
}
}
});
@ -163,8 +162,8 @@ $(function() {
detectedCode = result.codeResult.code;
$node = $('<li><div class="thumbnail"><div class="imgWrapper"><img /></div><div class="caption"><h4 class="code"></h4></div></div></li>');
$node.find("img").attr("src", canvas.toDataURL());
$node.find("h4.code").html(detectedCode);
$("#result_strip ul.thumbnails").prepend($node);
$node.find('img').attr('src', canvas.toDataURL());
$node.find('h4.code').html(detectedCode);
$('#result_strip ul.thumbnails').prepend($node);
});
});

@ -1,56 +1,49 @@
var path = require('path');
var webpack = require('webpack');
const webpackConfig = {
mode: 'production',
module: {
rules: [{
test: /\.(js|ts)$/,
exclude: /node_modules/,
loader: 'babel-loader'
}]
},
resolve: {
extensions: ['.js', '.ts']
}
};
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['mocha', 'chai', 'sinon', 'sinon-chai'],
frameworks: ['mocha', 'chai', 'sinon'],
files: [
'test/test-main-integration.js',
{pattern: 'test/integration/**/*.js', included: false},
{ pattern: 'test/integration/**/*.ts' },
{ pattern: 'test/fixtures/**/*.*', included: false }
],
preprocessors: {
'test/test-main-integration.js': ['webpack']
},
webpack: {
entry: [
'./src/quagga.js'
],
module: {
loaders: [{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel-loader'
}]
},
resolve: {
modules: [
path.resolve('./src/input/'),
path.resolve('./src/common/'),
'node_modules'
]
},
plugins: [
new webpack.DefinePlugin({
ENV: require(path.join(__dirname, './env/production'))
})
]
'**/*.ts': ['webpack']
},
webpack: webpackConfig,
plugins: [
'karma-chrome-launcher',
'karma-mocha',
'karma-chai',
'karma-sinon',
'karma-sinon-chai',
require('karma-webpack')
'karma-webpack'
],
reporters: ['progress'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO, // LOG_DEBUG
autoWatch: true,
browsers: ['Chrome'],
customLaunchers: {
ChromeDebugging: {
base: 'Chrome',
flags: ['--remote-debugging-port=9333'],
debug: true
}
},
browsers: ['ChromeDebugging'],
singleRun: false
});
};

@ -1,47 +1,33 @@
var path = require('path');
var webpack = require('webpack');
const webpackConfig = {
mode: 'development',
module: {
rules: [{
test: /\.(js|ts)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
envName: 'test'
}
}
}]
},
resolve: {
extensions: ['.js', '.ts']
}
};
module.exports = function(config) {
module.exports = config => {
config.set({
basePath: '',
frameworks: ['source-map-support', 'mocha', 'chai', 'sinon', 'sinon-chai'],
frameworks: ['source-map-support', 'mocha', 'chai', 'sinon'],
files: [
'test/test-main.js',
{pattern: 'test/spec/**/*.js', included: false}
{ pattern: 'test/spec/**/*.ts' }
],
preprocessors: {
'test/test-main.js': ['webpack']
},
webpack: {
entry: [
'./src/quagga.js'
],
module: {
loaders: [{
test: /\.jsx?$/,
exclude: [
path.resolve('node_modules/')
],
loader: 'babel-loader'
}, {
test: /\.js$/,
include: path.resolve('src'),
loader: 'babel-istanbul-loader'
}]
},
resolve: {
modules: [
path.resolve('./src/input/'),
path.resolve('./test/mocks/'),
'node_modules'
]
},
plugins: [
new webpack.DefinePlugin({
ENV: require(path.join(__dirname, './env/production'))
})
]
'**/*.ts': ['webpack']
},
webpack: webpackConfig,
plugins: [
'karma-chrome-launcher',
'karma-firefox-launcher',
@ -49,9 +35,8 @@ module.exports = function(config) {
'karma-mocha',
'karma-chai',
'karma-sinon',
'karma-sinon-chai',
'karma-source-map-support',
require('karma-webpack')
'karma-webpack'
],
reporters: ['progress', 'coverage'],
port: 9876,
@ -62,7 +47,7 @@ module.exports = function(config) {
singleRun: false,
coverageReporter: {
type: 'html',
dir: 'coverage/'
dir: './coverage'
}
});
};

@ -1,95 +0,0 @@
const CVUtils = require('../src/common/cv_utils'),
Ndarray = require("ndarray"),
Interp2D = require("ndarray-linear-interpolate").d2;
var FrameGrabber = {};
FrameGrabber.create = function(inputStream) {
var _that = {},
_streamConfig = inputStream.getConfig(),
_video_size = CVUtils.imageRef(inputStream.getRealWidth(), inputStream.getRealHeight()),
_canvasSize = inputStream.getCanvasSize(),
_size = CVUtils.imageRef(inputStream.getWidth(), inputStream.getHeight()),
_topRight = inputStream.getTopRight(),
_data = new Uint8Array(_size.x * _size.y),
_grayData = new Uint8Array(_video_size.x * _video_size.y),
_canvasData = new Uint8Array(_canvasSize.x * _canvasSize.y),
_grayImageArray = Ndarray(_grayData, [_video_size.y, _video_size.x]).transpose(1, 0),
_canvasImageArray = Ndarray(_canvasData, [_canvasSize.y, _canvasSize.x]).transpose(1, 0),
_targetImageArray = _canvasImageArray.hi(_topRight.x + _size.x, _topRight.y + _size.y).lo(_topRight.x, _topRight.y),
_stepSizeX = _video_size.x/_canvasSize.x,
_stepSizeY = _video_size.y/_canvasSize.y;
console.log("FrameGrabber", JSON.stringify({
videoSize: _grayImageArray.shape,
canvasSize: _canvasImageArray.shape,
stepSize: [_stepSizeX, _stepSizeY],
size: _targetImageArray.shape,
topRight: _topRight
}));
/**
* Uses the given array as frame-buffer
*/
_that.attachData = function(data) {
_data = data;
};
/**
* Returns the used frame-buffer
*/
_that.getData = function() {
return _data;
};
/**
* Fetches a frame from the input-stream and puts into the frame-buffer.
* The image-data is converted to gray-scale and then half-sampled if configured.
*/
_that.grab = function() {
var frame = inputStream.getFrame();
if (frame) {
this.scaleAndCrop(frame);
return true;
} else {
return false;
}
};
_that.scaleAndCrop = function(frame) {
var x,
y;
// 1. compute full-sized gray image
CVUtils.computeGray(frame.data, _grayData);
// 2. interpolate
for (y = 0; y < _canvasSize.y; y++) {
for (x = 0; x < _canvasSize.x; x++) {
_canvasImageArray.set(x, y, (Interp2D(_grayImageArray, x * _stepSizeX, y * _stepSizeY)) | 0);
}
}
// targetImageArray must be equal to targetSize
if (_targetImageArray.shape[0] !== _size.x ||
_targetImageArray.shape[1] !== _size.y) {
throw new Error("Shapes do not match!");
}
// 3. crop
for (y = 0; y < _size.y; y++) {
for (x = 0; x < _size.x; x++) {
_data[y * _size.x + x] = _targetImageArray.get(x, y);
}
}
},
_that.getSize = function() {
return _size;
};
return _that;
};
module.exports = FrameGrabber;

@ -1,153 +0,0 @@
const GetPixels = require("get-pixels");
var InputStream = {};
InputStream.createImageStream = function() {
var that = {};
var _config = null;
var width = 0,
height = 0,
frameIdx = 0,
paused = true,
loaded = false,
frame = null,
baseUrl,
ended = false,
size,
calculatedWidth,
calculatedHeight,
_eventNames = ['canrecord', 'ended'],
_eventHandlers = {},
_topRight = {x: 0, y: 0},
_canvasSize = {x: 0, y: 0};
function loadImages() {
loaded = false;
GetPixels(baseUrl, _config.mime, function(err, pixels) {
if (err) {
console.log(err);
exit(1);
}
loaded = true;
console.log(pixels.shape);
frame = pixels;
width = pixels.shape[0];
height = pixels.shape[1];
calculatedWidth = _config.size ? width/height > 1 ? _config.size : Math.floor((width/height) * _config.size) : width;
calculatedHeight = _config.size ? width/height > 1 ? Math.floor((height/width) * _config.size) : _config.size : height;
_canvasSize.x = calculatedWidth;
_canvasSize.y = calculatedHeight;
setTimeout(function() {
publishEvent("canrecord", []);
}, 0);
});
}
function publishEvent(eventName, args) {
var j,
handlers = _eventHandlers[eventName];
if (handlers && handlers.length > 0) {
for ( j = 0; j < handlers.length; j++) {
handlers[j].apply(that, args);
}
}
}
that.trigger = publishEvent;
that.getWidth = function() {
return calculatedWidth;
};
that.getHeight = function() {
return calculatedHeight;
};
that.setWidth = function(width) {
calculatedWidth = width;
};
that.setHeight = function(height) {
calculatedHeight = height;
};
that.getRealWidth = function() {
return width;
};
that.getRealHeight = function() {
return height;
};
that.setInputStream = function(stream) {
_config = stream;
baseUrl = _config.src;
size = 1;
loadImages();
};
that.ended = function() {
return ended;
};
that.setAttribute = function() {};
that.getConfig = function() {
return _config;
};
that.pause = function() {
paused = true;
};
that.play = function() {
paused = false;
};
that.setCurrentTime = function(time) {
frameIdx = time;
};
that.addEventListener = function(event, f) {
if (_eventNames.indexOf(event) !== -1) {
if (!_eventHandlers[event]) {
_eventHandlers[event] = [];
}
_eventHandlers[event].push(f);
}
};
that.setTopRight = function(topRight) {
_topRight.x = topRight.x;
_topRight.y = topRight.y;
};
that.getTopRight = function() {
return _topRight;
};
that.setCanvasSize = function(size) {
_canvasSize.x = size.x;
_canvasSize.y = size.y;
};
that.getCanvasSize = function() {
return _canvasSize;
};
that.getFrame = function() {
if (!loaded){
return null;
}
return frame;
};
return that;
};
module.exports = InputStream;

File diff suppressed because one or more lines are too long

10519
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,81 +1,54 @@
{
"name": "quagga",
"version": "0.12.1",
"description": "An advanced barcode-scanner written in JavaScript",
"main": "lib/quagga.js",
"typings": "type-definitions/quagga.d.ts",
"version": "0.14.0",
"description": "An advanced barcode-scanner written in JavaScript / TypeScript",
"main": "dist/quagga.node.js",
"browser": "dist/quagga.min.js",
"typings": "type-definitions/quagga.d.ts",
"typings": "dist/quagga.d.ts",
"devDependencies": {
"async": "^1.4.2",
"babel-cli": "^6.5.1",
"babel-core": "^6.21.0",
"babel-eslint": "^7.1.1",
"babel-istanbul": "^0.8.0",
"babel-istanbul-loader": "^0.1.0",
"babel-loader": "^6.2.10",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-check-es2015-constants": "^6.3.13",
"babel-plugin-lodash": "^3.2.11",
"babel-plugin-transform-es2015-arrow-functions": "^6.3.13",
"babel-plugin-transform-es2015-block-scoped-functions": "^6.3.13",
"babel-plugin-transform-es2015-block-scoping": "^6.21.0",
"babel-plugin-transform-es2015-classes": "^6.3.13",
"babel-plugin-transform-es2015-computed-properties": "^6.3.13",
"babel-plugin-transform-es2015-destructuring": "^6.3.13",
"babel-plugin-transform-es2015-for-of": "^6.3.13",
"babel-plugin-transform-es2015-function-name": "^6.3.13",
"babel-plugin-transform-es2015-literals": "^6.3.13",
"babel-plugin-transform-es2015-modules-commonjs": "^6.3.13",
"babel-plugin-transform-es2015-object-super": "^6.3.13",
"babel-plugin-transform-es2015-parameters": "^6.21.0",
"babel-plugin-transform-es2015-shorthand-properties": "^6.3.13",
"babel-plugin-transform-es2015-spread": "^6.3.13",
"babel-plugin-transform-es2015-sticky-regex": "^6.3.13",
"babel-plugin-transform-es2015-template-literals": "^6.3.13",
"babel-plugin-transform-es2015-typeof-symbol": "^6.3.13",
"babel-plugin-transform-es2015-unicode-regex": "^6.3.13",
"babel-plugin-transform-object-rest-spread": "^6.5.0",
"babel-plugin-transform-regenerator": "^6.21.0",
"chai": "^3.4.1",
"core-js": "^2.4.1",
"cross-env": "^3.1.4",
"eslint": "^1.10.3",
"grunt": "^0.4.5",
"grunt-cli": "^0.1.13",
"grunt-contrib-nodeunit": "^0.4.1",
"grunt-karma": "^2.0.0",
"isparta-loader": "^2.0.0",
"karma": "^1.3.0",
"@babel/core": "^7.5.5",
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
"@babel/plugin-transform-runtime": "^7.5.5",
"@babel/preset-env": "^7.5.5",
"@babel/preset-typescript": "^7.3.3",
"@types/async": "^3.0.1",
"@types/node": "^12.6.8",
"@types/sinon": "^7.0.13",
"async": "^3.1.0",
"babel-loader": "^8.0.6",
"babel-plugin-add-module-exports": "^1.0.2",
"babel-plugin-istanbul": "^5.2.0",
"chai": "^4.2.0",
"eslint": "^6.1.0",
"karma": "^4.2.0",
"karma-chai": "0.1.0",
"karma-chrome-launcher": "^2.0.0",
"karma-coverage": "^1.1.1",
"karma-firefox-launcher": "^0.1.7",
"karma-mocha": "~0.2.0",
"karma-phantomjs-launcher": "^0.2.1",
"karma-sinon": "^1.0.4",
"karma-sinon-chai": "^1.1.0",
"karma-source-map-support": "^1.1.0",
"karma-webpack": "^1.8.1",
"lolex": "^1.4.0",
"mocha": "^2.3.2",
"phantomjs": "^1.9.18",
"sinon": "^1.17.7",
"sinon-chai": "^2.8.0",
"webpack": "^2.2.1",
"webpack-sources": "^0.1.4"
"karma-chrome-launcher": "^3.0.0",
"karma-coverage": "^1.1.2",
"karma-firefox-launcher": "^1.1.0",
"karma-mocha": "~1.3.0",
"karma-sinon": "^1.0.5",
"karma-source-map-support": "^1.4.0",
"karma-webpack": "^4.0.2",
"mocha": "^6.2.0",
"sinon": "^7.3.2",
"typescript": "^3.5.3",
"webpack": "^4.38.0",
"webpack-cli": "^3.3.6",
"webpack-sources": "^1.3.0"
},
"directories": {
"doc": "doc"
},
"scripts": {
"test": "cross-env BABEL_ENV=commonjs grunt test",
"integrationtest": "grunt integrationtest",
"build:dev": "cross-env BUILD_ENV=development webpack",
"build:prod": "cross-env BUILD_ENV=production webpack --config webpack.config.min.js && grunt uglyasm",
"build:node": "cross-env BABEL_ENV=commonjs BUILD_ENV=node webpack --config webpack.node.config.js",
"build": "npm run build:dev && npm run build:prod && npm run build:node",
"watch": "cross-env BUILD_ENV=development webpack --watch",
"test": "karma start",
"integrationtest": "karma start karma-integration.conf.js",
"build:dev": "webpack",
"build:prod": "webpack --config webpack.prod.js",
"build:node": "webpack --config webpack.node.js",
"build:types": "tsc --emitDeclarationOnly",
"build": "npm run build:dev && npm run build:prod && npm run build:node && npm run build:types",
"watch": "webpack --watch",
"lint": "eslint src"
},
"repository": {
@ -99,17 +72,15 @@
"imageprocessing"
],
"author": "Christoph Oberhofer <ch.oberhofer@gmail.com>",
"contributors": [
"Alexei Domratchev <alexei.domratchev@dedal.ca>"
],
"license": "MIT",
"engines": {
"node": ">= 4.0"
"node": ">= 10.0"
},
"dependencies": {
"get-pixels": "^3.2.3",
"gl-mat2": "^1.0.0",
"gl-vec2": "^1.0.0",
"gl-vec3": "^1.0.3",
"lodash": "^4.17.4",
"ndarray": "^1.0.18",
"ndarray-linear-interpolate": "^1.0.0"
"get-pixels": "^3.3.2",
"ndarray": "^1.0.18"
}
}

@ -1,32 +1,46 @@
var ConcatSource = require("webpack-sources").ConcatSource;
var OriginalSource = require("webpack-sources").OriginalSource;
const ConcatSource = require('webpack-sources').ConcatSource;
const OriginalSource = require('webpack-sources').OriginalSource;
function MyUmdPlugin(options) {
this.name = options.library;
class UmdPlugin {
constructor() {
this._pluginName = 'UmdPlugin';
}
module.exports = MyUmdPlugin;
MyUmdPlugin.prototype.apply = function(compiler) {
compiler.plugin("this-compilation", function(compilation) {
var mainTemplate = compilation.mainTemplate;
compilation.templatesPlugin("render-with-entry", function(source, chunk, hash) {
return new ConcatSource(new OriginalSource(
"(function webpackUniversalModuleDefinition(root, factory) {\n" +
" if(typeof exports === 'object' && typeof module === 'object')\n" +
" module.exports = factory(factory.toString()).default;\n" +
" else if(typeof exports === 'object')\n" +
" exports[\"" + this.name + "\"] = factory(factory.toString()).default;\n" +
" else\n" +
" root[\"" + this.name + "\"] = factory(factory.toString()).default;\n" +
"})(this, function(__factorySource__) {\nreturn ", "webpack/myModuleDefinition"), source, "\n});\n");
}.bind(this));
mainTemplate.plugin("global-hash-paths", function(paths) {
if(this.name) paths = paths.concat(this.name);
apply(compiler) {
const name = compiler.options.output.library;
compiler.hooks.thisCompilation.tap(this._pluginName, compilation => {
const mainTemplate = compilation.mainTemplate;
mainTemplate.hooks.renderWithEntry.tap(this._pluginName, (source, _chunk, _hash) => {
return new ConcatSource(new OriginalSource(`
(function webpackUniversalModuleDefinition(root, factory) {
if (typeof exports === 'object' && typeof module === 'object') {
module.exports = factory(factory.toString()).default;
} else if (typeof exports === 'object') {
exports["${name}"] = factory(factory.toString()).default;
} else {
root["${name}"] = factory(factory.toString()).default;
}
})(this, function(__factorySource__) {
return `, 'webpack/myModuleDefinition'), source, `
});
`);
});
mainTemplate.hooks.globalHashPaths.tap(this._pluginName, paths => {
if (name) {
paths = paths.concat(name);
}
return paths;
}.bind(this));
mainTemplate.plugin("hash", function(hash) {
hash.update("umd");
hash.update(this.name + "");
}.bind(this));
}.bind(this));
};
});
mainTemplate.hooks.hash.tap(this._pluginName, hash => {
hash.update('umd');
hash.update(name);
});
});
}
}
module.exports = UmdPlugin;

@ -0,0 +1,63 @@
import { ImageDebug } from '../common/image-debug';
import { QuaggaBarcode } from '../decoder/barcode-decoder';
import { Barcode } from '../reader/barcode-reader';
export interface ResultCollectorConfig {
capacity?: number;
capture?: boolean;
blacklist?: Array<Barcode>;
filter?: (item: Barcode) => boolean;
}
export class ResultCollector {
private _canvas: HTMLCanvasElement;
private _context: CanvasRenderingContext2D;
private _config: ResultCollectorConfig;
private _capacity: number;
private _capture: boolean;
private _results: Array<QuaggaBarcode>;
constructor(config: ResultCollectorConfig) {
this._results = new Array<QuaggaBarcode>();
this._config = config;
this._capacity = config.capacity || 20;
this._capture = config.capture === true;
if (this._capture) {
this._canvas = document.createElement('canvas');
this._context = this._canvas.getContext('2d');
}
}
addResult(data: Uint8Array, imageWidth: number, imageHeight: number, codeResult: Barcode): void {
if (codeResult && this._capacity && !this._contains(codeResult) && this._passesFilter(codeResult)) {
const result: QuaggaBarcode = { codeResult };
this._capacity--;
if (this._capture) {
this._canvas.width = imageWidth;
this._canvas.height = imageHeight;
ImageDebug.drawImage(data, imageWidth, imageHeight, this._context);
result.frame = this._canvas.toDataURL();
}
this._results.push(result);
}
}
getResults(): Array<QuaggaBarcode> {
return this._results;
}
private _contains(codeResult: Barcode): boolean {
return this._config.blacklist &&
this._config.blacklist.some(item => Object.keys(item).every(key => item[key] === codeResult[key]));
}
private _passesFilter(codeResult: Barcode): boolean {
return typeof this._config.filter !== 'function' || this._config.filter(codeResult);
}
}

@ -1,57 +0,0 @@
import ImageDebug from '../common/image_debug';
function contains(codeResult, list) {
if (list) {
return list.some(function (item) {
return Object.keys(item).every(function (key) {
return item[key] === codeResult[key];
});
});
}
return false;
}
function passesFilter(codeResult, filter) {
if (typeof filter === 'function') {
return filter(codeResult);
}
return true;
}
export default {
create: function(config) {
var canvas = document.createElement("canvas"),
ctx = canvas.getContext("2d"),
results = [],
capacity = config.capacity || 20,
capture = config.capture === true;
function matchesConstraints(codeResult) {
return capacity
&& codeResult
&& !contains(codeResult, config.blacklist)
&& passesFilter(codeResult, config.filter);
}
return {
addResult: function(data, imageSize, codeResult) {
var result = {};
if (matchesConstraints(codeResult)) {
capacity--;
result.codeResult = codeResult;
if (capture) {
canvas.width = imageSize.x;
canvas.height = imageSize.y;
ImageDebug.drawImage(data, imageSize, ctx);
result.frame = canvas.toDataURL();
}
results.push(result);
}
},
getResults: function() {
return results;
}
};
}
};

@ -1,79 +0,0 @@
export default {
init: function(arr, val) {
var l = arr.length;
while (l--) {
arr[l] = val;
}
},
/**
* Shuffles the content of an array
* @return {Array} the array itself shuffled
*/
shuffle: function(arr) {
var i = arr.length - 1, j, x;
for (i; i >= 0; i--) {
j = Math.floor(Math.random() * i);
x = arr[i];
arr[i] = arr[j];
arr[j] = x;
}
return arr;
},
toPointList: function(arr) {
var i, j, row = [], rows = [];
for ( i = 0; i < arr.length; i++) {
row = [];
for ( j = 0; j < arr[i].length; j++) {
row[j] = arr[i][j];
}
rows[i] = "[" + row.join(",") + "]";
}
return "[" + rows.join(",\r\n") + "]";
},
/**
* returns the elements which's score is bigger than the threshold
* @return {Array} the reduced array
*/
threshold: function(arr, threshold, scoreFunc) {
var i, queue = [];
for ( i = 0; i < arr.length; i++) {
if (scoreFunc.apply(arr, [arr[i]]) >= threshold) {
queue.push(arr[i]);
}
}
return queue;
},
maxIndex: function(arr) {
var i, max = 0;
for ( i = 0; i < arr.length; i++) {
if (arr[i] > arr[max]) {
max = i;
}
}
return max;
},
max: function(arr) {
var i, max = 0;
for ( i = 0; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
},
sum: function(arr) {
var length = arr.length,
sum = 0;
while (length--) {
sum += arr[length];
}
return sum;
}
};

@ -0,0 +1,3 @@
import { Point } from "./point";
export type Box = [Point, Point, Point, Point];

@ -1,68 +0,0 @@
const vec2 = {
clone: require('gl-vec2/clone'),
dot: require('gl-vec2/dot')
}
/**
* Creates a cluster for grouping similar orientations of datapoints
*/
export default {
create: function(point, threshold) {
var points = [],
center = {
rad: 0,
vec: vec2.clone([0, 0])
},
pointMap = {};
function init() {
add(point);
updateCenter();
}
function add(pointToAdd) {
pointMap[pointToAdd.id] = pointToAdd;
points.push(pointToAdd);
}
function updateCenter() {
var i, sum = 0;
for ( i = 0; i < points.length; i++) {
sum += points[i].rad;
}
center.rad = sum / points.length;
center.vec = vec2.clone([Math.cos(center.rad), Math.sin(center.rad)]);
}
init();
return {
add: function(pointToAdd) {
if (!pointMap[pointToAdd.id]) {
add(pointToAdd);
updateCenter();
}
},
fits: function(otherPoint) {
// check cosine similarity to center-angle
var similarity = Math.abs(vec2.dot(otherPoint.point.vec, center.vec));
if (similarity > threshold) {
return true;
}
return false;
},
getPoints: function() {
return points;
},
getCenter: function() {
return center;
}
};
},
createPoint: function(newPoint, id, property) {
return {
rad: newPoint[property],
point: newPoint,
id: id
};
}
};

@ -0,0 +1,60 @@
import { Moment } from './moment';
/**
* Creates a cluster for grouping similar orientations of moments
*/
export class Cluster {
private _threshold: number;
private _moments: Array<Moment>;
private _center: Moment;
static clusterize(moments: Array<Moment>, threshold: number): Array<Cluster> {
const clusters = new Array<Cluster>();
moments.forEach(moment => {
const matchingCluster = clusters.find(cluster => cluster.fits(moment));
if (matchingCluster) {
matchingCluster.add(moment);
} else {
clusters.push(new Cluster(threshold, moment));
}
});
return clusters;
}
constructor(threshold: number, moment: Moment) {
this._threshold = threshold;
this._moments = new Array<Moment>();
this._center = {
rad: 0,
x: 0,
y: 0
};
if (moment) {
this.add(moment);
}
}
add(point: Moment) {
this._moments.push(point);
// Update center
this._center.rad = this._moments.reduce((sum, p) => sum + p.rad, 0) / this._moments.length;
this._center.x = Math.cos(this._center.rad);
this._center.y = Math.sin(this._center.rad);
}
fits(moment: Moment): boolean {
// check cosine similarity to center-angle
const similarity = Math.abs(moment.x * this._center.x + moment.y * this._center.y);
return similarity > this._threshold;
}
get moments() {
return this._moments;
}
}

@ -1,751 +0,0 @@
import Cluster2 from './cluster';
import ArrayHelper from './array_helper';
const vec2 = {
clone: require('gl-vec2/clone'),
};
const vec3 = {
clone: require('gl-vec3/clone'),
};
/**
* @param x x-coordinate
* @param y y-coordinate
* @return ImageReference {x,y} Coordinate
*/
export function imageRef(x, y) {
var that = {
x: x,
y: y,
toVec2: function() {
return vec2.clone([this.x, this.y]);
},
toVec3: function() {
return vec3.clone([this.x, this.y, 1]);
},
round: function() {
this.x = this.x > 0.0 ? Math.floor(this.x + 0.5) : Math.floor(this.x - 0.5);
this.y = this.y > 0.0 ? Math.floor(this.y + 0.5) : Math.floor(this.y - 0.5);
return this;
}
};
return that;
};
/**
* Computes an integral image of a given grayscale image.
* @param imageDataContainer {ImageDataContainer} the image to be integrated
*/
export function computeIntegralImage2(imageWrapper, integralWrapper) {
var imageData = imageWrapper.data;
var width = imageWrapper.size.x;
var height = imageWrapper.size.y;
var integralImageData = integralWrapper.data;
var sum = 0, posA = 0, posB = 0, posC = 0, posD = 0, x, y;
// sum up first column
posB = width;
sum = 0;
for ( y = 1; y < height; y++) {
sum += imageData[posA];
integralImageData[posB] += sum;
posA += width;
posB += width;
}
posA = 0;
posB = 1;
sum = 0;
for ( x = 1; x < width; x++) {
sum += imageData[posA];
integralImageData[posB] += sum;
posA++;
posB++;
}
for ( y = 1; y < height; y++) {
posA = y * width + 1;
posB = (y - 1) * width + 1;
posC = y * width;
posD = (y - 1) * width;
for ( x = 1; x < width; x++) {
integralImageData[posA] +=
imageData[posA] + integralImageData[posB] + integralImageData[posC] - integralImageData[posD];
posA++;
posB++;
posC++;
posD++;
}
}
};
export function computeIntegralImage(imageWrapper, integralWrapper) {
var imageData = imageWrapper.data;
var width = imageWrapper.size.x;
var height = imageWrapper.size.y;
var integralImageData = integralWrapper.data;
var sum = 0;
// sum up first row
for (var i = 0; i < width; i++) {
sum += imageData[i];
integralImageData[i] = sum;
}
for (var v = 1; v < height; v++) {
sum = 0;
for (var u = 0; u < width; u++) {
sum += imageData[v * width + u];
integralImageData[((v) * width) + u] = sum + integralImageData[(v - 1) * width + u];
}
}
};
export function thresholdImage(imageWrapper, threshold, targetWrapper) {
if (!targetWrapper) {
targetWrapper = imageWrapper;
}
var imageData = imageWrapper.data, length = imageData.length, targetData = targetWrapper.data;
while (length--) {
targetData[length] = imageData[length] < threshold ? 1 : 0;
}
};
export function computeHistogram(imageWrapper, bitsPerPixel) {
if (!bitsPerPixel) {
bitsPerPixel = 8;
}
var imageData = imageWrapper.data,
length = imageData.length,
bitShift = 8 - bitsPerPixel,
bucketCnt = 1 << bitsPerPixel,
hist = new Int32Array(bucketCnt);
while (length--) {
hist[imageData[length] >> bitShift]++;
}
return hist;
};
export function sharpenLine(line) {
var i,
length = line.length,
left = line[0],
center = line[1],
right;
for (i = 1; i < length - 1; i++) {
right = line[i + 1];
// -1 4 -1 kernel
line[i - 1] = (((center * 2) - left - right)) & 255;
left = center;
center = right;
}
return line;
};
export function determineOtsuThreshold(imageWrapper, bitsPerPixel) {
if (!bitsPerPixel) {
bitsPerPixel = 8;
}
var hist,
threshold,
bitShift = 8 - bitsPerPixel;
function px(init, end) {
var sum = 0, i;
for ( i = init; i <= end; i++) {
sum += hist[i];
}
return sum;
}
function mx(init, end) {
var i, sum = 0;
for ( i = init; i <= end; i++) {
sum += i * hist[i];
}
return sum;
}
function determineThreshold() {
var vet = [0], p1, p2, p12, k, m1, m2, m12,
max = (1 << bitsPerPixel) - 1;
hist = computeHistogram(imageWrapper, bitsPerPixel);
for ( k = 1; k < max; k++) {
p1 = px(0, k);
p2 = px(k + 1, max);
p12 = p1 * p2;
if (p12 === 0) {
p12 = 1;
}
m1 = mx(0, k) * p2;
m2 = mx(k + 1, max) * p1;
m12 = m1 - m2;
vet[k] = m12 * m12 / p12;
}
return ArrayHelper.maxIndex(vet);
}
threshold = determineThreshold();
return threshold << bitShift;
};
export function otsuThreshold(imageWrapper, targetWrapper) {
var threshold = determineOtsuThreshold(imageWrapper);
thresholdImage(imageWrapper, threshold, targetWrapper);
return threshold;
};
// local thresholding
export function computeBinaryImage(imageWrapper, integralWrapper, targetWrapper) {
computeIntegralImage(imageWrapper, integralWrapper);
if (!targetWrapper) {
targetWrapper = imageWrapper;
}
var imageData = imageWrapper.data;
var targetData = targetWrapper.data;
var width = imageWrapper.size.x;
var height = imageWrapper.size.y;
var integralImageData = integralWrapper.data;
var sum = 0, v, u, kernel = 3, A, B, C, D, avg, size = (kernel * 2 + 1) * (kernel * 2 + 1);
// clear out top & bottom-border
for ( v = 0; v <= kernel; v++) {
for ( u = 0; u < width; u++) {
targetData[((v) * width) + u] = 0;
targetData[(((height - 1) - v) * width) + u] = 0;
}
}
// clear out left & right border
for ( v = kernel; v < height - kernel; v++) {
for ( u = 0; u <= kernel; u++) {
targetData[((v) * width) + u] = 0;
targetData[((v) * width) + (width - 1 - u)] = 0;
}
}
for ( v = kernel + 1; v < height - kernel - 1; v++) {
for ( u = kernel + 1; u < width - kernel; u++) {
A = integralImageData[(v - kernel - 1) * width + (u - kernel - 1)];
B = integralImageData[(v - kernel - 1) * width + (u + kernel)];
C = integralImageData[(v + kernel) * width + (u - kernel - 1)];
D = integralImageData[(v + kernel) * width + (u + kernel)];
sum = D - C - B + A;
avg = sum / (size);
targetData[v * width + u] = imageData[v * width + u] > (avg + 5) ? 0 : 1;
}
}
};
export function cluster(points, threshold, property) {
var i, k, cluster, point, clusters = [];
if (!property) {
property = "rad";
}
function addToCluster(newPoint) {
var found = false;
for ( k = 0; k < clusters.length; k++) {
cluster = clusters[k];
if (cluster.fits(newPoint)) {
cluster.add(newPoint);
found = true;
}
}
return found;
}
// iterate over each cloud
for ( i = 0; i < points.length; i++) {
point = Cluster2.createPoint(points[i], i, property);
if (!addToCluster(point)) {
clusters.push(Cluster2.create(point, threshold));
}
}
return clusters;
};
export const Tracer = {
trace: function(points, vec) {
var iteration, maxIterations = 10, top = [], result = [], centerPos = 0, currentPos = 0;
function trace(idx, forward) {
var from, to, toIdx, predictedPos, thresholdX = 1, thresholdY = Math.abs(vec[1] / 10), found = false;
function match(pos, predicted) {
if (pos.x > (predicted.x - thresholdX)
&& pos.x < (predicted.x + thresholdX)
&& pos.y > (predicted.y - thresholdY)
&& pos.y < (predicted.y + thresholdY)) {
return true;
} else {
return false;
}
}
// check if the next index is within the vec specifications
// if not, check as long as the threshold is met
from = points[idx];
if (forward) {
predictedPos = {
x: from.x + vec[0],
y: from.y + vec[1]
};
} else {
predictedPos = {
x: from.x - vec[0],
y: from.y - vec[1]
};
}
toIdx = forward ? idx + 1 : idx - 1;
to = points[toIdx];
while (to && ( found = match(to, predictedPos)) !== true && (Math.abs(to.y - from.y) < vec[1])) {
toIdx = forward ? toIdx + 1 : toIdx - 1;
to = points[toIdx];
}
return found ? toIdx : null;
}
for ( iteration = 0; iteration < maxIterations; iteration++) {
// randomly select point to start with
centerPos = Math.floor(Math.random() * points.length);
// trace forward
top = [];
currentPos = centerPos;
top.push(points[currentPos]);
while (( currentPos = trace(currentPos, true)) !== null) {
top.push(points[currentPos]);
}
if (centerPos > 0) {
currentPos = centerPos;
while (( currentPos = trace(currentPos, false)) !== null) {
top.push(points[currentPos]);
}
}
if (top.length > result.length) {
result = top;
}
}
return result;
}
};
export const DILATE = 1;
export const ERODE = 2;
export function dilate(inImageWrapper, outImageWrapper) {
var v,
u,
inImageData = inImageWrapper.data,
outImageData = outImageWrapper.data,
height = inImageWrapper.size.y,
width = inImageWrapper.size.x,
sum,
yStart1,
yStart2,
xStart1,
xStart2;
for ( v = 1; v < height - 1; v++) {
for ( u = 1; u < width - 1; u++) {
yStart1 = v - 1;
yStart2 = v + 1;
xStart1 = u - 1;
xStart2 = u + 1;
sum = inImageData[yStart1 * width + xStart1] + inImageData[yStart1 * width + xStart2] +
inImageData[v * width + u] +
inImageData[yStart2 * width + xStart1] + inImageData[yStart2 * width + xStart2];
outImageData[v * width + u] = sum > 0 ? 1 : 0;
}
}
};
export function erode(inImageWrapper, outImageWrapper) {
var v,
u,
inImageData = inImageWrapper.data,
outImageData = outImageWrapper.data,
height = inImageWrapper.size.y,
width = inImageWrapper.size.x,
sum,
yStart1,
yStart2,
xStart1,
xStart2;
for ( v = 1; v < height - 1; v++) {
for ( u = 1; u < width - 1; u++) {
yStart1 = v - 1;
yStart2 = v + 1;
xStart1 = u - 1;
xStart2 = u + 1;
sum = inImageData[yStart1 * width + xStart1] + inImageData[yStart1 * width + xStart2] +
inImageData[v * width + u] +
inImageData[yStart2 * width + xStart1] + inImageData[yStart2 * width + xStart2];
outImageData[v * width + u] = sum === 5 ? 1 : 0;
}
}
};
export function subtract(aImageWrapper, bImageWrapper, resultImageWrapper) {
if (!resultImageWrapper) {
resultImageWrapper = aImageWrapper;
}
var length = aImageWrapper.data.length,
aImageData = aImageWrapper.data,
bImageData = bImageWrapper.data,
cImageData = resultImageWrapper.data;
while (length--) {
cImageData[length] = aImageData[length] - bImageData[length];
}
};
export function bitwiseOr(aImageWrapper, bImageWrapper, resultImageWrapper) {
if (!resultImageWrapper) {
resultImageWrapper = aImageWrapper;
}
var length = aImageWrapper.data.length,
aImageData = aImageWrapper.data,
bImageData = bImageWrapper.data,
cImageData = resultImageWrapper.data;
while (length--) {
cImageData[length] = aImageData[length] || bImageData[length];
}
};
export function countNonZero(imageWrapper) {
var length = imageWrapper.data.length, data = imageWrapper.data, sum = 0;
while (length--) {
sum += data[length];
}
return sum;
};
export function topGeneric(list, top, scoreFunc) {
var i, minIdx = 0, min = 0, queue = [], score, hit, pos;
for ( i = 0; i < top; i++) {
queue[i] = {
score: 0,
item: null
};
}
for ( i = 0; i < list.length; i++) {
score = scoreFunc.apply(this, [list[i]]);
if (score > min) {
hit = queue[minIdx];
hit.score = score;
hit.item = list[i];
min = Number.MAX_VALUE;
for ( pos = 0; pos < top; pos++) {
if (queue[pos].score < min) {
min = queue[pos].score;
minIdx = pos;
}
}
}
}
return queue;
};
export function grayArrayFromImage(htmlImage, offsetX, ctx, array) {
ctx.drawImage(htmlImage, offsetX, 0, htmlImage.width, htmlImage.height);
var ctxData = ctx.getImageData(offsetX, 0, htmlImage.width, htmlImage.height).data;
computeGray(ctxData, array);
};
export function grayArrayFromContext(ctx, size, offset, array) {
var ctxData = ctx.getImageData(offset.x, offset.y, size.x, size.y).data;
computeGray(ctxData, array);
};
export function grayAndHalfSampleFromCanvasData(canvasData, size, outArray) {
var topRowIdx = 0;
var bottomRowIdx = size.x;
var endIdx = Math.floor(canvasData.length / 4);
var outWidth = size.x / 2;
var outImgIdx = 0;
var inWidth = size.x;
var i;
while (bottomRowIdx < endIdx) {
for ( i = 0; i < outWidth; i++) {
outArray[outImgIdx] = (
(0.299 * canvasData[topRowIdx * 4 + 0] +
0.587 * canvasData[topRowIdx * 4 + 1] +
0.114 * canvasData[topRowIdx * 4 + 2]) +
(0.299 * canvasData[(topRowIdx + 1) * 4 + 0] +
0.587 * canvasData[(topRowIdx + 1) * 4 + 1] +
0.114 * canvasData[(topRowIdx + 1) * 4 + 2]) +
(0.299 * canvasData[(bottomRowIdx) * 4 + 0] +
0.587 * canvasData[(bottomRowIdx) * 4 + 1] +
0.114 * canvasData[(bottomRowIdx) * 4 + 2]) +
(0.299 * canvasData[(bottomRowIdx + 1) * 4 + 0] +
0.587 * canvasData[(bottomRowIdx + 1) * 4 + 1] +
0.114 * canvasData[(bottomRowIdx + 1) * 4 + 2])) / 4;
outImgIdx++;
topRowIdx = topRowIdx + 2;
bottomRowIdx = bottomRowIdx + 2;
}
topRowIdx = topRowIdx + inWidth;
bottomRowIdx = bottomRowIdx + inWidth;
}
};
export function computeGray(imageData, outArray, config) {
var l = (imageData.length / 4) | 0,
i,
singleChannel = config && config.singleChannel === true;
if (singleChannel) {
for (i = 0; i < l; i++) {
outArray[i] = imageData[i * 4 + 0];
}
} else {
for (i = 0; i < l; i++) {
outArray[i] =
0.299 * imageData[i * 4 + 0] + 0.587 * imageData[i * 4 + 1] + 0.114 * imageData[i * 4 + 2];
}
}
};
export function loadImageArray(src, callback, canvas) {
if (!canvas) {
canvas = document.createElement('canvas');
}
var img = new Image();
img.callback = callback;
img.onload = function() {
canvas.width = this.width;
canvas.height = this.height;
var ctx = canvas.getContext('2d');
ctx.drawImage(this, 0, 0);
var array = new Uint8Array(this.width * this.height);
ctx.drawImage(this, 0, 0);
var data = ctx.getImageData(0, 0, this.width, this.height).data;
computeGray(data, array);
this.callback(array, {
x: this.width,
y: this.height
}, this);
};
img.src = src;
};
/**
* @param inImg {ImageWrapper} input image to be sampled
* @param outImg {ImageWrapper} to be stored in
*/
export function halfSample(inImgWrapper, outImgWrapper) {
var inImg = inImgWrapper.data;
var inWidth = inImgWrapper.size.x;
var outImg = outImgWrapper.data;
var topRowIdx = 0;
var bottomRowIdx = inWidth;
var endIdx = inImg.length;
var outWidth = inWidth / 2;
var outImgIdx = 0;
while (bottomRowIdx < endIdx) {
for (var i = 0; i < outWidth; i++) {
outImg[outImgIdx] = Math.floor(
(inImg[topRowIdx] + inImg[topRowIdx + 1] + inImg[bottomRowIdx] + inImg[bottomRowIdx + 1]) / 4);
outImgIdx++;
topRowIdx = topRowIdx + 2;
bottomRowIdx = bottomRowIdx + 2;
}
topRowIdx = topRowIdx + inWidth;
bottomRowIdx = bottomRowIdx + inWidth;
}
};
export function hsv2rgb(hsv, rgb) {
var h = hsv[0],
s = hsv[1],
v = hsv[2],
c = v * s,
x = c * (1 - Math.abs((h / 60) % 2 - 1)),
m = v - c,
r = 0,
g = 0,
b = 0;
rgb = rgb || [0, 0, 0];
if (h < 60) {
r = c;
g = x;
} else if (h < 120) {
r = x;
g = c;
} else if (h < 180) {
g = c;
b = x;
} else if (h < 240) {
g = x;
b = c;
} else if (h < 300) {
r = x;
b = c;
} else if (h < 360) {
r = c;
b = x;
}
rgb[0] = ((r + m) * 255) | 0;
rgb[1] = ((g + m) * 255) | 0;
rgb[2] = ((b + m) * 255) | 0;
return rgb;
};
export function _computeDivisors(n) {
var largeDivisors = [],
divisors = [],
i;
for (i = 1; i < Math.sqrt(n) + 1; i++) {
if (n % i === 0) {
divisors.push(i);
if (i !== n / i) {
largeDivisors.unshift(Math.floor(n / i));
}
}
}
return divisors.concat(largeDivisors);
};
function _computeIntersection(arr1, arr2) {
var i = 0,
j = 0,
result = [];
while (i < arr1.length && j < arr2.length) {
if (arr1[i] === arr2[j]) {
result.push(arr1[i]);
i++;
j++;
} else if (arr1[i] > arr2[j]) {
j++;
} else {
i++;
}
}
return result;
};
export function calculatePatchSize(patchSize, imgSize) {
var divisorsX = _computeDivisors(imgSize.x),
divisorsY = _computeDivisors(imgSize.y),
wideSide = Math.max(imgSize.x, imgSize.y),
common = _computeIntersection(divisorsX, divisorsY),
nrOfPatchesList = [8, 10, 15, 20, 32, 60, 80],
nrOfPatchesMap = {
"x-small": 5,
"small": 4,
"medium": 3,
"large": 2,
"x-large": 1
},
nrOfPatchesIdx = nrOfPatchesMap[patchSize] || nrOfPatchesMap.medium,
nrOfPatches = nrOfPatchesList[nrOfPatchesIdx],
desiredPatchSize = Math.floor(wideSide / nrOfPatches),
optimalPatchSize;
function findPatchSizeForDivisors(divisors) {
var i = 0,
found = divisors[Math.floor(divisors.length / 2)];
while (i < (divisors.length - 1) && divisors[i] < desiredPatchSize) {
i++;
}
if (i > 0) {
if (Math.abs(divisors[i] - desiredPatchSize) > Math.abs(divisors[i - 1] - desiredPatchSize)) {
found = divisors[i - 1];
} else {
found = divisors[i];
}
}
if (desiredPatchSize / found < nrOfPatchesList[nrOfPatchesIdx + 1] / nrOfPatchesList[nrOfPatchesIdx] &&
desiredPatchSize / found > nrOfPatchesList[nrOfPatchesIdx - 1] / nrOfPatchesList[nrOfPatchesIdx] ) {
return {x: found, y: found};
}
return null;
}
optimalPatchSize = findPatchSizeForDivisors(common);
if (!optimalPatchSize) {
optimalPatchSize = findPatchSizeForDivisors(_computeDivisors(wideSide));
if (!optimalPatchSize) {
optimalPatchSize = findPatchSizeForDivisors((_computeDivisors(desiredPatchSize * nrOfPatches)));
}
}
return optimalPatchSize;
};
export function _parseCSSDimensionValues(value) {
var dimension = {
value: parseFloat(value),
unit: value.indexOf("%") === value.length - 1 ? "%" : "%"
};
return dimension;
};
export const _dimensionsConverters = {
top: function(dimension, context) {
if (dimension.unit === "%") {
return Math.floor(context.height * (dimension.value / 100));
}
},
right: function(dimension, context) {
if (dimension.unit === "%") {
return Math.floor(context.width - (context.width * (dimension.value / 100)));
}
},
bottom: function(dimension, context) {
if (dimension.unit === "%") {
return Math.floor(context.height - (context.height * (dimension.value / 100)));
}
},
left: function(dimension, context) {
if (dimension.unit === "%") {
return Math.floor(context.width * (dimension.value / 100));
}
}
};
export function computeImageArea(inputWidth, inputHeight, area) {
var context = {width: inputWidth, height: inputHeight};
var parsedArea = Object.keys(area).reduce(function(result, key) {
var value = area[key],
parsed = _parseCSSDimensionValues(value),
calculated = _dimensionsConverters[key](parsed, context);
result[key] = calculated;
return result;
}, {});
return {
sx: parsedArea.left,
sy: parsedArea.top,
sw: parsedArea.right - parsedArea.left,
sh: parsedArea.bottom - parsedArea.top
};
};

@ -1,94 +0,0 @@
export default (function() {
var events = {};
function getEvent(eventName) {
if (!events[eventName]) {
events[eventName] = {
subscribers: []
};
}
return events[eventName];
}
function clearEvents(){
events = {};
}
function publishSubscription(subscription, data) {
if (subscription.async) {
setTimeout(function() {
subscription.callback(data);
}, 4);
} else {
subscription.callback(data);
}
}
function subscribe(event, callback, async) {
var subscription;
if ( typeof callback === "function") {
subscription = {
callback: callback,
async: async
};
} else {
subscription = callback;
if (!subscription.callback) {
throw "Callback was not specified on options";
}
}
getEvent(event).subscribers.push(subscription);
}
return {
subscribe: function(event, callback, async) {
return subscribe(event, callback, async);
},
publish: function(eventName, data) {
var event = getEvent(eventName),
subscribers = event.subscribers;
// Publish one-time subscriptions
subscribers.filter(function(subscriber) {
return !!subscriber.once;
}).forEach((subscriber) => {
publishSubscription(subscriber, data);
});
// remove them from the subscriber
event.subscribers = subscribers.filter(function(subscriber) {
return !subscriber.once;
});
// publish the rest
event.subscribers.forEach((subscriber) => {
publishSubscription(subscriber, data);
});
},
once: function(event, callback, async) {
subscribe(event, {
callback: callback,
async: async,
once: true
});
},
unsubscribe: function(eventName, callback) {
var event;
if (eventName) {
event = getEvent(eventName);
if (event && callback) {
event.subscribers = event.subscribers.filter(function(subscriber){
return subscriber.callback !== callback;
});
} else {
event.subscribers = [];
}
} else {
clearEvents();
}
}
};
})();

@ -0,0 +1,85 @@
export interface EventCallback {
(data: any): void;
}
export interface EventSubscription {
callback: EventCallback;
async?: boolean;
once?: boolean;
}
interface EventItem {
subscriptions: Array<EventSubscription>
}
let events: { [name: string]: EventItem } = {};
export class Events {
static subscribe(event: string, callback: EventSubscription | EventCallback, async?: boolean) {
let subscription: EventSubscription;
if (typeof callback === 'function') {
subscription = {
callback,
async
};
} else {
subscription = callback;
if (!subscription.callback) {
throw 'Callback was not specified on options';
}
}
getEvent(event).subscriptions.push(subscription);
}
static publish(type: string, data?: any) {
const eventItem = getEvent(type);
const subscriptions = eventItem.subscriptions;
// Publish one-time subscriptions
subscriptions.filter(({ once }) => !!once).forEach(subscription => publishSubscription(subscription, data));
// remove them from the subscription
eventItem.subscriptions = subscriptions.filter(({ once }) => !once);
// publish the rest
eventItem.subscriptions.forEach(subscription => publishSubscription(subscription, data));
}
static once(event: string, callback: EventCallback, async?: boolean): void {
Events.subscribe(event, { callback, async, once: true });
}
static unsubscribe(eventName?: string, callback?: EventCallback) {
if (eventName) {
const event = getEvent(eventName);
if (event && callback) {
event.subscriptions = event.subscriptions.filter(subscription => subscription.callback !== callback);
} else {
event.subscriptions = [];
}
} else {
events = {};
}
}
}
function getEvent(eventName: string): EventItem {
if (!events[eventName]) {
events[eventName] = {
subscriptions: []
};
}
return events[eventName];
}
function publishSubscription(subscription: EventSubscription, data: any): void {
if (subscription.async) {
setTimeout(function () {
subscription.callback(data);
}, 4);
} else {
subscription.callback(data);
}
}

@ -0,0 +1,43 @@
export type HSV = [number, number, number];
export type RGB = [number, number, number];
export function hsv2rgb(hsv: HSV, rgb?: RGB): RGB {
const h = hsv[0];
const s = hsv[1];
const v = hsv[2];
const c = v * s;
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
const m = v - c;
let r = 0;
let g = 0;
let b = 0;
if (h < 60) {
r = c;
g = x;
} else if (h < 120) {
r = x;
g = c;
} else if (h < 180) {
g = c;
b = x;
} else if (h < 240) {
g = x;
b = c;
} else if (h < 300) {
r = x;
b = c;
} else if (h < 360) {
r = c;
b = x;
}
rgb = rgb || [0, 0, 0];
rgb[0] = (r + m) * 255 | 0;
rgb[1] = (g + m) * 255 | 0;
rgb[2] = (b + m) * 255 | 0;
return rgb;
}

@ -0,0 +1,39 @@
import { Point } from './point';
export const ImageDebug = {
drawPath(path: Array<Point>, context: CanvasRenderingContext2D, color: string, lineWidth: number): void {
if (path && path.length > 1) {
context.strokeStyle = color;
context.fillStyle = color;
context.lineWidth = lineWidth;
context.beginPath();
context.moveTo(path[0].x, path[0].y);
path.slice(1).forEach(({ x, y }) => context.lineTo(x, y));
context.closePath();
context.stroke();
}
},
drawImage(imageData: Uint8Array, width: number, height: number, context: CanvasRenderingContext2D): boolean {
const canvasData = context.getImageData(0, 0, width, height);
const data = canvasData.data;
let imageIndex = imageData.length | 0;
let canvasIndex = data.length | 0;
if (canvasIndex / imageIndex !== 4) {
return false;
}
while (imageIndex--) {
const value = imageData[imageIndex];
data[--canvasIndex] = 255;
data[--canvasIndex] = value;
data[--canvasIndex] = value;
data[--canvasIndex] = value;
}
context.putImageData(canvasData, 0, 0);
return true;
}
}

@ -0,0 +1,244 @@
import { Moment } from './moment';
import { Point } from './point';
import { HSV, hsv2rgb, RGB } from './hsv2rgb';
type ArrayType = Array<number> | Uint8Array | Int32Array;
/**
* Represents a basic image combining the data and size.
* In addition, some methods for manipulation are contained.
*/
export class ImageWrapper<T extends ArrayType = Uint8Array> {
data: T | Uint8Array;
size: Point;
/**
* @param size The size of the image in pixel
* @param data If given, a flat array containing the pixel data
* @param arrayType If given, the desired DataType of the Array (may be typed/non-typed)
* @param initialize Indicating if the array should be initialized on creation.
*/
constructor(size: Point, data?: T, arrayType?: { new(_: number): T | Uint8Array }, initialize?: boolean) {
if (!data) {
this.data = new (arrayType || Uint8Array)(size.x * size.y);
if (initialize) {
this.data.fill(0);
}
} else {
this.data = data;
}
this.size = size;
}
/**
* Tests if a position is within the image with a given offset
* @param point The location to test
* @param border The padding value in pixels
* @returns true if location inside the image's border, false otherwise
* @see cvd/image.h
*/
inImageWithBorder(point: Point, border: number): boolean {
return (point.x >= border)
&& (point.y >= border)
&& (point.x < (this.size.x - border))
&& (point.y < (this.size.y - border));
}
/**
* Creates an {ImageWrapper) and copies the needed underlying image-data area
* @param imageWrapper The target {ImageWrapper} where the data should be copied
* @param fromX The horizontal position where to copy from
* @param fromY The vertical position where to copy from
*/
subImageAsCopy(imageWrapper: ImageWrapper, fromX: number, fromY: number): void {
const sizeY = imageWrapper.size.y;
const sizeX = imageWrapper.size.x;
for (let x = 0; x < sizeX; x++) {
for (let y = 0; y < sizeY; y++) {
imageWrapper.data[y * sizeX + x] = this.data[(fromY + y) * this.size.x + fromX + x];
}
}
}
/**
* Retrieves a given pixel position from the image
* @param x The x-position
* @param y The y-position
* @returns The grayscale value at the pixel-position
*/
get(x: number, y: number): number {
return this.data[y * this.size.x + x];
}
/**
* Sets a given pixel position in the image
* @param x The x-position
* @param y The y-position
* @param value The grayscale value to set
* @returns The Image itself (for possible chaining)
*/
set(x: number, y: number, value: number): ImageWrapper<T> {
this.data[y * this.size.x + x] = value;
return this;
}
/**
* Sets the border of the image (1 pixel) to zero
*/
zeroBorder(): void {
const width = this.size.x;
const height = this.size.y;
const data = this.data;
for (let i = 0; i < width; i++) {
data[i] = data[(height - 1) * width + i] = 0;
}
for (let i = 1; i < height - 1; i++) {
data[i * width] = data[i * width + (width - 1)] = 0;
}
}
/**
* Inverts a binary image in place
*/
invert(): void {
const data = this.data;
for (let i = data.length; i--;) {
data[i] = data[i] ? 0 : 1;
}
}
moments(labelCount: number): Array<Moment> {
const height = this.size.y;
const width = this.size.x;
const labelSum = new Array<Moment>();
const result = new Array<Moment>();
if (labelCount <= 0) {
return result;
}
for (let i = 0; i < labelCount; i++) {
labelSum[i] = {
m00: 0,
m01: 0,
m10: 0,
m11: 0,
m02: 0,
m20: 0,
theta: 0,
rad: 0
};
}
for (let y = 0; y < height; y++) {
const ysq = y * y;
for (let x = 0; x < width; x++) {
const val = this.data[y * width + x];
if (val > 0) {
const label = labelSum[val - 1];
label.m00 += 1;
label.m01 += y;
label.m10 += x;
label.m11 += x * y;
label.m02 += ysq;
label.m20 += x * x;
}
}
}
const PI = Math.PI;
const PI_4 = PI / 4;
for (let i = 0; i < labelCount; i++) {
const label = labelSum[i];
if (!isNaN(label.m00) && label.m00 !== 0) {
const x_ = label.m10 / label.m00;
const y_ = label.m01 / label.m00;
const mu11 = label.m11 / label.m00 - x_ * y_;
const mu02 = label.m02 / label.m00 - y_ * y_;
const mu20 = label.m20 / label.m00 - x_ * x_;
const tmp = 0.5 * Math.atan((mu02 - mu20) / (2 * mu11)) + (mu11 >= 0 ? PI_4 : -PI_4) + PI;
label.theta = (tmp * 180 / PI + 90) % 180 - 90;
if (label.theta < 0) {
label.theta += 180;
}
label.rad = tmp > PI ? tmp - PI : tmp;
label.x = Math.cos(tmp);
label.y = Math.sin(tmp);
result.push(label);
}
}
return result;
}
/**
* Displays the {ImageWrapper} in a given canvas
* @param context The rendering context to write to
* @param scale Scale which is applied to each pixel-value
*/
show(context: CanvasRenderingContext2D, scale: number): void {
const height = this.size.y;
const width = this.size.x;
// const context = canvas.getContext('2d');
// canvas.height = height;
// canvas.width = width;
const frame = context.getImageData(0, 0, width, height);
const data = frame.data;
let current = 0;
if (!scale) {
scale = 1.0;
}
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const pixel = y * width + x;
current = this.get(x, y) * scale;
data[pixel * 4 + 0] = current;
data[pixel * 4 + 1] = current;
data[pixel * 4 + 2] = current;
data[pixel * 4 + 3] = 255;
}
}
//frame.data = data;
context.putImageData(frame, 0, 0);
}
/**
* Displays the part of the image in a given canvas
* @param context The rendering context to write to
* @param scale Scale which is applied to each pixel-value
* @param fromX The horizontal position where to overlay from
* @param fromY The vertical position where to overlay from
*/
overlay(context: CanvasRenderingContext2D, scale: number, fromX: number, fromY: number): void {
const hsv: HSV = [0, 1, 1];
const whiteRgb: RGB = [255, 255, 255];
const blackRgb: RGB = [0, 0, 0];
const frame = context.getImageData(fromX, fromY, this.size.x, this.size.y);
const data = frame.data;
if (!scale || scale < 0 || scale > 360) {
scale = 360;
}
for (let length = this.data.length; length--;) {
hsv[0] = this.data[length] * scale;
const rgb: RGB = hsv[0] <= 0 ? whiteRgb : hsv[0] >= 360 ? blackRgb : hsv2rgb(hsv);
data[length * 4 + 0] = rgb[0];
data[length * 4 + 1] = rgb[1];
data[length * 4 + 2] = rgb[2];
data[length * 4 + 3] = 255;
}
context.putImageData(frame, fromX, fromY);
}
}

@ -1,41 +0,0 @@
export default {
drawRect: function(pos, size, ctx, style){
ctx.strokeStyle = style.color;
ctx.fillStyle = style.color;
ctx.lineWidth = style.lineWidth || 1;
ctx.beginPath();
ctx.strokeRect(pos.x, pos.y, size.x, size.y);
},
drawPath: function(path, def, ctx, style) {
ctx.strokeStyle = style.color;
ctx.fillStyle = style.color;
ctx.lineWidth = style.lineWidth;
ctx.beginPath();
ctx.moveTo(path[0][def.x], path[0][def.y]);
for (var j = 1; j < path.length; j++) {
ctx.lineTo(path[j][def.x], path[j][def.y]);
}
ctx.closePath();
ctx.stroke();
},
drawImage: function(imageData, size, ctx) {
var canvasData = ctx.getImageData(0, 0, size.x, size.y),
data = canvasData.data,
imageDataPos = imageData.length,
canvasDataPos = data.length,
value;
if (canvasDataPos / imageDataPos !== 4) {
return false;
}
while (imageDataPos--){
value = imageData[imageDataPos];
data[--canvasDataPos] = 255;
data[--canvasDataPos] = value;
data[--canvasDataPos] = value;
data[--canvasDataPos] = value;
}
ctx.putImageData(canvasData, 0, 0);
return true;
}
};

@ -1,349 +0,0 @@
import SubImage from './subImage';
import {hsv2rgb} from '../common/cv_utils';
import ArrayHelper from '../common/array_helper';
const vec2 = {
clone: require('gl-vec2/clone'),
};
/**
* Represents a basic image combining the data and size.
* In addition, some methods for manipulation are contained.
* @param size {x,y} The size of the image in pixel
* @param data {Array} If given, a flat array containing the pixel data
* @param ArrayType {Type} If given, the desired DataType of the Array (may be typed/non-typed)
* @param initialize {Boolean} Indicating if the array should be initialized on creation.
* @returns {ImageWrapper}
*/
function ImageWrapper(size, data, ArrayType, initialize) {
if (!data) {
if (ArrayType) {
this.data = new ArrayType(size.x * size.y);
if (ArrayType === Array && initialize) {
ArrayHelper.init(this.data, 0);
}
} else {
this.data = new Uint8Array(size.x * size.y);
if (Uint8Array === Array && initialize) {
ArrayHelper.init(this.data, 0);
}
}
} else {
this.data = data;
}
this.size = size;
}
/**
* tests if a position is within the image with a given offset
* @param imgRef {x, y} The location to test
* @param border Number the padding value in pixel
* @returns {Boolean} true if location inside the image's border, false otherwise
* @see cvd/image.h
*/
ImageWrapper.prototype.inImageWithBorder = function(imgRef, border) {
return (imgRef.x >= border)
&& (imgRef.y >= border)
&& (imgRef.x < (this.size.x - border))
&& (imgRef.y < (this.size.y - border));
};
/**
* Performs bilinear sampling
* @param inImg Image to extract sample from
* @param x the x-coordinate
* @param y the y-coordinate
* @returns the sampled value
* @see cvd/vision.h
*/
ImageWrapper.sample = function(inImg, x, y) {
var lx = Math.floor(x);
var ly = Math.floor(y);
var w = inImg.size.x;
var base = ly * inImg.size.x + lx;
var a = inImg.data[base + 0];
var b = inImg.data[base + 1];
var c = inImg.data[base + w];
var d = inImg.data[base + w + 1];
var e = a - b;
x -= lx;
y -= ly;
var result = Math.floor(x * (y * (e - c + d) - e) + y * (c - a) + a);
return result;
};
/**
* Initializes a given array. Sets each element to zero.
* @param array {Array} The array to initialize
*/
ImageWrapper.clearArray = function(array) {
var l = array.length;
while (l--) {
array[l] = 0;
}
};
/**
* Creates a {SubImage} from the current image ({this}).
* @param from {ImageRef} The position where to start the {SubImage} from. (top-left corner)
* @param size {ImageRef} The size of the resulting image
* @returns {SubImage} A shared part of the original image
*/
ImageWrapper.prototype.subImage = function(from, size) {
return new SubImage(from, size, this);
};
/**
* Creates an {ImageWrapper) and copies the needed underlying image-data area
* @param imageWrapper {ImageWrapper} The target {ImageWrapper} where the data should be copied
* @param from {ImageRef} The location where to copy from (top-left location)
*/
ImageWrapper.prototype.subImageAsCopy = function(imageWrapper, from) {
var sizeY = imageWrapper.size.y, sizeX = imageWrapper.size.x;
var x, y;
for ( x = 0; x < sizeX; x++) {
for ( y = 0; y < sizeY; y++) {
imageWrapper.data[y * sizeX + x] = this.data[(from.y + y) * this.size.x + from.x + x];
}
}
};
ImageWrapper.prototype.copyTo = function(imageWrapper) {
var length = this.data.length, srcData = this.data, dstData = imageWrapper.data;
while (length--) {
dstData[length] = srcData[length];
}
};
/**
* Retrieves a given pixel position from the image
* @param x {Number} The x-position
* @param y {Number} The y-position
* @returns {Number} The grayscale value at the pixel-position
*/
ImageWrapper.prototype.get = function(x, y) {
return this.data[y * this.size.x + x];
};
/**
* Retrieves a given pixel position from the image
* @param x {Number} The x-position
* @param y {Number} The y-position
* @returns {Number} The grayscale value at the pixel-position
*/
ImageWrapper.prototype.getSafe = function(x, y) {
var i;
if (!this.indexMapping) {
this.indexMapping = {
x: [],
y: []
};
for (i = 0; i < this.size.x; i++) {
this.indexMapping.x[i] = i;
this.indexMapping.x[i + this.size.x] = i;
}
for (i = 0; i < this.size.y; i++) {
this.indexMapping.y[i] = i;
this.indexMapping.y[i + this.size.y] = i;
}
}
return this.data[(this.indexMapping.y[y + this.size.y]) * this.size.x + this.indexMapping.x[x + this.size.x]];
};
/**
* Sets a given pixel position in the image
* @param x {Number} The x-position
* @param y {Number} The y-position
* @param value {Number} The grayscale value to set
* @returns {ImageWrapper} The Image itself (for possible chaining)
*/
ImageWrapper.prototype.set = function(x, y, value) {
this.data[y * this.size.x + x] = value;
return this;
};
/**
* Sets the border of the image (1 pixel) to zero
*/
ImageWrapper.prototype.zeroBorder = function() {
var i, width = this.size.x, height = this.size.y, data = this.data;
for ( i = 0; i < width; i++) {
data[i] = data[(height - 1) * width + i] = 0;
}
for ( i = 1; i < height - 1; i++) {
data[i * width] = data[i * width + (width - 1)] = 0;
}
};
/**
* Inverts a binary image in place
*/
ImageWrapper.prototype.invert = function() {
var data = this.data, length = data.length;
while (length--) {
data[length] = data[length] ? 0 : 1;
}
};
ImageWrapper.prototype.convolve = function(kernel) {
var x, y, kx, ky, kSize = (kernel.length / 2) | 0, accu = 0;
for ( y = 0; y < this.size.y; y++) {
for ( x = 0; x < this.size.x; x++) {
accu = 0;
for ( ky = -kSize; ky <= kSize; ky++) {
for ( kx = -kSize; kx <= kSize; kx++) {
accu += kernel[ky + kSize][kx + kSize] * this.getSafe(x + kx, y + ky);
}
}
this.data[y * this.size.x + x] = accu;
}
}
};
ImageWrapper.prototype.moments = function(labelcount) {
var data = this.data,
x,
y,
height = this.size.y,
width = this.size.x,
val,
ysq,
labelsum = [],
i,
label,
mu11,
mu02,
mu20,
x_,
y_,
tmp,
result = [],
PI = Math.PI,
PI_4 = PI / 4;
if (labelcount <= 0) {
return result;
}
for ( i = 0; i < labelcount; i++) {
labelsum[i] = {
m00: 0,
m01: 0,
m10: 0,
m11: 0,
m02: 0,
m20: 0,
theta: 0,
rad: 0
};
}
for ( y = 0; y < height; y++) {
ysq = y * y;
for ( x = 0; x < width; x++) {
val = data[y * width + x];
if (val > 0) {
label = labelsum[val - 1];
label.m00 += 1;
label.m01 += y;
label.m10 += x;
label.m11 += x * y;
label.m02 += ysq;
label.m20 += x * x;
}
}
}
for ( i = 0; i < labelcount; i++) {
label = labelsum[i];
if (!isNaN(label.m00) && label.m00 !== 0) {
x_ = label.m10 / label.m00;
y_ = label.m01 / label.m00;
mu11 = label.m11 / label.m00 - x_ * y_;
mu02 = label.m02 / label.m00 - y_ * y_;
mu20 = label.m20 / label.m00 - x_ * x_;
tmp = (mu02 - mu20) / (2 * mu11);
tmp = 0.5 * Math.atan(tmp) + (mu11 >= 0 ? PI_4 : -PI_4 ) + PI;
label.theta = (tmp * 180 / PI + 90) % 180 - 90;
if (label.theta < 0) {
label.theta += 180;
}
label.rad = tmp > PI ? tmp - PI : tmp;
label.vec = vec2.clone([Math.cos(tmp), Math.sin(tmp)]);
result.push(label);
}
}
return result;
};
/**
* Displays the {ImageWrapper} in a given canvas
* @param canvas {Canvas} The canvas element to write to
* @param scale {Number} Scale which is applied to each pixel-value
*/
ImageWrapper.prototype.show = function(canvas, scale) {
var ctx,
frame,
data,
current,
pixel,
x,
y;
if (!scale) {
scale = 1.0;
}
ctx = canvas.getContext('2d');
canvas.width = this.size.x;
canvas.height = this.size.y;
frame = ctx.getImageData(0, 0, canvas.width, canvas.height);
data = frame.data;
current = 0;
for (y = 0; y < this.size.y; y++) {
for (x = 0; x < this.size.x; x++) {
pixel = y * this.size.x + x;
current = this.get(x, y) * scale;
data[pixel * 4 + 0] = current;
data[pixel * 4 + 1] = current;
data[pixel * 4 + 2] = current;
data[pixel * 4 + 3] = 255;
}
}
//frame.data = data;
ctx.putImageData(frame, 0, 0);
};
/**
* Displays the {SubImage} in a given canvas
* @param canvas {Canvas} The canvas element to write to
* @param scale {Number} Scale which is applied to each pixel-value
*/
ImageWrapper.prototype.overlay = function(canvas, scale, from) {
if (!scale || scale < 0 || scale > 360) {
scale = 360;
}
var hsv = [0, 1, 1];
var rgb = [0, 0, 0];
var whiteRgb = [255, 255, 255];
var blackRgb = [0, 0, 0];
var result = [];
var ctx = canvas.getContext('2d');
var frame = ctx.getImageData(from.x, from.y, this.size.x, this.size.y);
var data = frame.data;
var length = this.data.length;
while (length--) {
hsv[0] = this.data[length] * scale;
result = hsv[0] <= 0 ? whiteRgb : hsv[0] >= 360 ? blackRgb : hsv2rgb(hsv, rgb);
data[length * 4 + 0] = result[0];
data[length * 4 + 1] = result[1];
data[length * 4 + 2] = result[2];
data[length * 4 + 3] = 255;
}
ctx.putImageData(frame, from.x, from.y);
};
export default ImageWrapper;

@ -0,0 +1,14 @@
export function enumerateDevices(): Promise<Array<MediaDeviceInfo>> {
if (navigator.mediaDevices && typeof navigator.mediaDevices.enumerateDevices === 'function') {
return navigator.mediaDevices.enumerateDevices();
}
return Promise.reject(new Error('enumerateDevices is not defined'));
}
export function getUserMedia(constraints: MediaStreamConstraints): Promise<MediaStream> {
if (navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia === 'function') {
return navigator.mediaDevices.getUserMedia(constraints);
}
return Promise.reject(new Error('getUserMedia is not defined'));
}

@ -1,17 +0,0 @@
export function enumerateDevices() {
if (navigator.mediaDevices
&& typeof navigator.mediaDevices.enumerateDevices === 'function') {
return navigator.mediaDevices.enumerateDevices();
}
return Promise.reject(new Error('enumerateDevices is not defined'));
};
export function getUserMedia(constraints) {
if (navigator.mediaDevices
&& typeof navigator.mediaDevices.getUserMedia === 'function') {
return navigator.mediaDevices
.getUserMedia(constraints);
}
return Promise.reject(new Error('getUserMedia is not defined'));
}

@ -0,0 +1,31 @@
/**
* Performs a deep merge of objects and returns new object.
* Does not modify objects (immutable).
* @see https://stackoverflow.com/a/48218209
*
* @param objects - Objects to merge
* @returns New object with merged key/values
*/
export function merge(...objects: ReadonlyArray<any>): object {
const isObject = (obj: unknown) => obj && typeof obj === 'object';
return objects.reduce((prev, obj) => {
if (obj) {
Object.keys(obj).forEach(key => {
const pVal = prev[key];
const oVal = obj[key];
if (Array.isArray(pVal) && Array.isArray(oVal)) {
// prev[key] = pVal.concat(...oVal);
prev[key] = oVal;
} else if (isObject(pVal) && isObject(oVal)) {
prev[key] = merge(pVal, oVal);
} else {
prev[key] = oVal;
}
});
}
return prev;
}, {});
}

@ -0,0 +1,12 @@
export interface Moment {
x?: number;
y?: number;
m00?: number;
m01?: number;
m10?: number;
m11?: number;
m02?: number;
m20?: number;
theta?: number;
rad?: number;
}

@ -0,0 +1,4 @@
export interface Point {
x: number;
y: number;
}

@ -0,0 +1,21 @@
export function polyfills(): void {
if (!Math.imul) {
Math.imul = function (opA: number, opB: number): number {
opB |= 0; // ensure that opB is an integer. opA will automatically be coerced.
// floating points give us 53 bits of precision to work with plus 1 sign bit
// automatically handled for our convienence:
// 1. 0x003fffff /*opA & 0x000fffff*/ * 0x7fffffff /*opB*/ = 0x1fffff7fc00001
// 0x1fffff7fc00001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/
let result = (opA & 0x003fffff) * opB;
// 2. We can remove an integer coersion from the statement above because:
// 0x1fffff7fc00001 + 0xffc00000 = 0x1fffffff800001
// 0x1fffffff800001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/
if (opA & 0xffc00000 /*!== 0*/) {
result += (opA & 0xffc00000) * opB | 0;
}
return result | 0;
};
}
}
export default polyfills();

@ -1,90 +0,0 @@
/**
* Construct representing a part of another {ImageWrapper}. Shares data
* between the parent and the child.
* @param from {ImageRef} The position where to start the {SubImage} from. (top-left corner)
* @param size {ImageRef} The size of the resulting image
* @param I {ImageWrapper} The {ImageWrapper} to share from
* @returns {SubImage} A shared part of the original image
*/
function SubImage(from, size, I) {
if (!I) {
I = {
data: null,
size: size
};
}
this.data = I.data;
this.originalSize = I.size;
this.I = I;
this.from = from;
this.size = size;
}
/**
* Displays the {SubImage} in a given canvas
* @param canvas {Canvas} The canvas element to write to
* @param scale {Number} Scale which is applied to each pixel-value
*/
SubImage.prototype.show = function(canvas, scale) {
var ctx,
frame,
data,
current,
y,
x,
pixel;
if (!scale) {
scale = 1.0;
}
ctx = canvas.getContext('2d');
canvas.width = this.size.x;
canvas.height = this.size.y;
frame = ctx.getImageData(0, 0, canvas.width, canvas.height);
data = frame.data;
current = 0;
for (y = 0; y < this.size.y; y++) {
for (x = 0; x < this.size.x; x++) {
pixel = y * this.size.x + x;
current = this.get(x, y) * scale;
data[pixel * 4 + 0] = current;
data[pixel * 4 + 1] = current;
data[pixel * 4 + 2] = current;
data[pixel * 4 + 3] = 255;
}
}
frame.data = data;
ctx.putImageData(frame, 0, 0);
};
/**
* Retrieves a given pixel position from the {SubImage}
* @param x {Number} The x-position
* @param y {Number} The y-position
* @returns {Number} The grayscale value at the pixel-position
*/
SubImage.prototype.get = function(x, y) {
return this.data[(this.from.y + y) * this.originalSize.x + this.from.x + x];
};
/**
* Updates the underlying data from a given {ImageWrapper}
* @param image {ImageWrapper} The updated image
*/
SubImage.prototype.updateData = function(image) {
this.originalSize = image.size;
this.data = image.data;
};
/**
* Updates the position of the shared area
* @param from {x,y} The new location
* @returns {SubImage} returns {this} for possible chaining
*/
SubImage.prototype.updateFrom = function(from) {
this.from = from;
return this;
};
export default (SubImage);

@ -1,51 +0,0 @@
/*
* typedefs.js
* Normalizes browser-specific prefixes
*/
if (typeof window !== 'undefined') {
window.requestAnimFrame = (function () {
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (/* function FrameRequestCallback */ callback) {
window.setTimeout(callback, 1000 / 60);
};
})();
}
Math.imul = Math.imul || function(a, b) {
var ah = (a >>> 16) & 0xffff,
al = a & 0xffff,
bh = (b >>> 16) & 0xffff,
bl = b & 0xffff;
// the shift by 0 fixes the sign on the high part
// the final |0 converts the unsigned value into a signed value
return ((al * bl) + (((ah * bl + al * bh) << 16) >>> 0) | 0);
};
if (typeof Object.assign !== 'function') {
Object.assign = function(target) { // .length of function is 2
'use strict';
if (target === null) { // TypeError if undefined or null
throw new TypeError('Cannot convert undefined or null to object');
}
var to = Object(target);
for (var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index];
if (nextSource !== null) { // Skip over if undefined or null
for (var nextKey in nextSource) {
// Avoid bugs when hasOwnProperty is shadowed
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
};
}

@ -1,19 +1,21 @@
module.exports = {
import { QuaggaConfig } from './config';
export const config: QuaggaConfig = {
inputStream: {
name: "Live",
type: "LiveStream",
name: 'Live',
type: 'LiveStream',
constraints: {
width: 640,
height: 480,
// aspectRatio: 640/480, // optional
facingMode: "environment", // or user
// deviceId: "38745983457387598375983759834"
facingMode: 'environment' // or user
// deviceId: '38745983457387598375983759834'
},
area: {
top: "0%",
right: "0%",
left: "0%",
bottom: "0%"
top: '0%',
right: '0%',
left: '0%',
bottom: '0%'
},
singleChannel: false // true: only the red color-channel is read
},
@ -32,7 +34,7 @@ module.exports = {
},
locator: {
halfSample: true,
patchSize: "medium", // x-small, small, medium, large, x-large
patchSize: 'medium', // x-small, small, medium, large, x-large
debug: {
showCanvas: false,
showPatches: false,

@ -1,11 +0,0 @@
let config;
if (ENV.development){
config = require('./config.dev.js');
} else if (ENV.node) {
config = require('./config.node.js');
} else {
config = require('./config.prod.js');
}
export default config;

@ -1,13 +1,15 @@
module.exports = {
import { QuaggaConfig } from './config';
export const config: QuaggaConfig = {
inputStream: {
type: "ImageStream",
type: 'ImageStream',
sequence: false,
size: 800,
area: {
top: "0%",
right: "0%",
left: "0%",
bottom: "0%"
top: '0%',
right: '0%',
left: '0%',
bottom: '0%'
},
singleChannel: false // true: only the red color-channel is read
},
@ -20,6 +22,6 @@ module.exports = {
},
locator: {
halfSample: true,
patchSize: "medium" // x-small, small, medium, large, x-large
patchSize: 'medium' // x-small, small, medium, large, x-large
}
};

@ -1,31 +0,0 @@
module.exports = {
inputStream: {
name: "Live",
type: "LiveStream",
constraints: {
width: 640,
height: 480,
// aspectRatio: 640/480, // optional
facingMode: "environment", // or user
// deviceId: "38745983457387598375983759834"
},
area: {
top: "0%",
right: "0%",
left: "0%",
bottom: "0%"
},
singleChannel: false // true: only the red color-channel is read
},
locate: true,
numOfWorkers: 4,
decoder: {
readers: [
'code_128_reader'
]
},
locator: {
halfSample: true,
patchSize: "medium" // x-small, small, medium, large, x-large
}
};

@ -0,0 +1,51 @@
import { BarcodeDecoderConfig } from '../decoder/barcode-decoder';
import { BarcodeLocatorConfig } from '../locator/barcode-locator-config';
import { InputStreamConfig } from '../input/input-stream-config';
import { BarcodeReaderDeclaration } from '../reader/barcode-reader';
interface DebugConfig {
}
export interface QuaggaConfig {
debug?: DebugConfig;
decoder?: BarcodeDecoderConfig;
frequency?: number;
inputStream?: InputStreamConfig;
locate?: boolean;
locator?: BarcodeLocatorConfig;
numOfWorkers?: number;
src?: string;
}
export const config: QuaggaConfig = {
inputStream: {
name: 'Live',
type: 'LiveStream',
constraints: {
width: 640,
height: 480,
// aspectRatio: 640/480, // optional
facingMode: 'environment' // or user
// deviceId: '38745983457387598375983759834'
},
area: {
top: '0%',
right: '0%',
left: '0%',
bottom: '0%'
},
singleChannel: false // true: only the red color-channel is read
},
locate: true,
numOfWorkers: 4,
decoder: {
readers: [
'code_128_reader'
]
},
locator: {
halfSample: true,
patchSize: 'medium' // x-small, small, medium, large, x-large
}
};

@ -0,0 +1,317 @@
import { Box } from '../common/box';
import { ImageWrapper } from '../common/image-wrapper';
import { Point } from '../common/point';
import { Readers } from '../reader/index';
import { Barcode, BarcodeReader, BarcodeReaderConfig, BarcodeReaderDeclaration, BarcodeReaderType } from '../reader/barcode-reader';
import { BarcodeLine, Bresenham } from './bresenham';
import { ImageDebug } from '../common/image-debug';
export interface BarcodeDecoderConfig {
debug?: {
drawBoundingBox?: boolean;
drawScanline?: boolean;
showFrequency?: boolean;
showPattern?: boolean;
};
multiple?: boolean;
readers?: Array<BarcodeReaderDeclaration>;
}
type Line = [Point, Point];
export interface QuaggaBarcode {
angle?: number;
barcodes?: Array<QuaggaBarcode>; // TOOD: deal with multiple results
box?: Box;
boxes?: Array<Box>; // TOOD: deal with multiple results
codeResult?: Barcode;
frame?: string;
line?: Line;
pattern?: Array<number>;
threshold?: number;
};
interface BarcodeAndBarcodeLine {
codeResult: Barcode;
barcodeLine: BarcodeLine;
}
export class BarcodeDecoder {
private _config: BarcodeDecoderConfig;
private _inputImageWrapper: ImageWrapper<Uint8Array>;
private _frequencyCanvas: HTMLCanvasElement;
private _patternCanvas: HTMLCanvasElement;
private _overlayContext: CanvasRenderingContext2D;
private _barcodeReaders: Array<BarcodeReader>;
constructor(config: BarcodeDecoderConfig, inputImageWrapper: ImageWrapper<Uint8Array>) {
this._config = config;
this._inputImageWrapper = inputImageWrapper;
this._barcodeReaders = [];
if (process.env.NODE_ENV !== 'production' && this._config.debug && typeof document !== 'undefined') {
const debugDiv = document.querySelector('#debug.detection');
this._frequencyCanvas = document.querySelector('canvas.frequency');
if (!this._frequencyCanvas) {
this._frequencyCanvas = document.createElement('canvas');
this._frequencyCanvas.className = 'frequency';
if (debugDiv) {
debugDiv.appendChild(this._frequencyCanvas);
}
}
this._frequencyCanvas.style.display = this._config.debug.showFrequency ? 'block' : 'none';
this._patternCanvas = document.querySelector('canvas.patternBuffer');
if (!this._patternCanvas) {
this._patternCanvas = document.createElement('canvas');
this._patternCanvas.className = 'patternBuffer';
if (debugDiv) {
debugDiv.appendChild(this._patternCanvas);
}
}
this._patternCanvas.style.display = this._config.debug.showPattern ? 'block' : 'none';
const overlayCanvas = document.querySelector<HTMLCanvasElement>('canvas.drawingBuffer');
this._overlayContext = overlayCanvas ? overlayCanvas.getContext('2d') : null;
}
this._initReaders();
}
decodeFromBoundingBoxes(boxes: Array<Box>): QuaggaBarcode {
let barcode: QuaggaBarcode = null;
if (boxes) {
if (this._config.multiple) {
const barcodes = boxes.map(box => this.decodeFromBoundingBox(box));
return { barcodes, boxes };
}
if (boxes.some(box => !!(barcode = this.decodeFromBoundingBox(box)))) {
barcode.boxes = boxes;
}
}
return barcode;
}
/**
* With the help of the configured readers this function tries to detect
* a valid barcode pattern within the given area.
* @param box The area to search in
* @returns The result {codeResult, line, angle, pattern, threshold}
*/
decodeFromBoundingBox(box: Box): QuaggaBarcode {
const debug = process.env.NODE_ENV !== 'production' && this._overlayContext && this._config.debug;
if (debug && debug.drawBoundingBox) {
this._drawPath(box, 'blue', 2);
}
let line = this._getLine(box);
if (line === null) {
return null;
}
const angle = Math.atan2(line[1].y - line[0].y, line[1].x - line[0].x);
line = this._getExtendedLine(line, angle);
let result = this._tryDecode(line);
if (result === null) {
result = this._tryDecodeBruteForce(box, line, angle);
}
if (result === null) {
return null;
}
if (debug && debug.drawScanline) {
this._drawPath(line, 'red', 3);
}
return {
angle,
box,
codeResult: result.codeResult,
line,
pattern: result.barcodeLine.line,
threshold: result.barcodeLine.threshold
};
}
setReaders(readers: Array<BarcodeReaderDeclaration>): void {
this._config.readers = readers;
this._barcodeReaders.length = 0;
this._initReaders();
}
private _initReaders(): void {
this._config.readers.forEach(readerConfig => {
let reader: BarcodeReaderType;
let configuration: BarcodeReaderConfig = {};
let supplements = [];
if (typeof readerConfig === 'object') {
reader = readerConfig.format;
configuration = readerConfig.config || {};
} else if (typeof readerConfig === 'string') {
reader = readerConfig;
}
if (process.env.NODE_ENV !== 'production') {
console.log('Before registering reader:', reader);
}
if (configuration.supplements) {
supplements = configuration.supplements.map(supplement => new Readers[supplement]());
}
this._barcodeReaders.push(new Readers[reader](configuration, supplements));
});
if (process.env.NODE_ENV !== 'production') {
console.log('Registered Readers:',
...this._barcodeReaders.map(({ config, FORMAT }) => JSON.stringify({ config, FORMAT })));
}
}
/**
* extend the line on both ends
* @param line
* @param angle
*/
private _getExtendedLine(line: Line, angle: number): Line {
function extendLine(amount: number) {
const extension = {
y: amount * Math.sin(angle),
x: amount * Math.cos(angle)
};
line[0].y -= extension.y;
line[0].x -= extension.x;
line[1].y += extension.y;
line[1].x += extension.x;
}
const lineLength = Math.sqrt((line[1].y - line[0].y) ** 2 + (line[1].x - line[0].x) ** 2);
let extensionLength = lineLength * 0.1 | 0;
extendLine(extensionLength);
// check if inside image
while (extensionLength > 1 && (!this._inputImageWrapper.inImageWithBorder(line[0], 0)
|| !this._inputImageWrapper.inImageWithBorder(line[1], 0))) {
extensionLength >>= 1;
extendLine(-extensionLength);
}
return line;
}
private _getLine(box: Box): Line {
return [{
x: (box[1].x + box[0].x) / 2,
y: (box[1].y + box[0].y) / 2
}, {
x: (box[3].x + box[2].x) / 2,
y: (box[3].y + box[2].y) / 2
}];
}
private _tryDecode(line: Line): BarcodeAndBarcodeLine {
const debug = process.env.NODE_ENV !== 'production' && this._config.debug;
if (debug && this._overlayContext) {
this._drawPath(line, 'red', 3);
}
let barcodeLine = Bresenham.getBarcodeLine(this._inputImageWrapper, line[0], line[1]);
if (debug && debug.showFrequency) {
this._printFrequency(barcodeLine.line);
}
barcodeLine = Bresenham.toBinaryLine(barcodeLine);
if (debug && debug.showPattern) {
this._printPattern(barcodeLine.line);
}
let codeResult: Barcode = null;
this._barcodeReaders.some(reader => !!(codeResult = reader.decodePattern(barcodeLine.line)));
return codeResult ? { codeResult, barcodeLine } : null;
}
/**
* This method slices the given area apart and tries to detect a barcode-pattern for each slice.
* It returns the decoded barcode, or null if nothing was found
* @param box
* @param line
* @param lineAngle
*/
private _tryDecodeBruteForce(box: Box, line: Line, lineAngle: number): BarcodeAndBarcodeLine {
const sideLength = Math.sqrt((box[1].x - box[0].x) ** 2 + (box[1].y - box[0].y) ** 2);
const slices = 16;
const xdir = Math.sin(lineAngle);
const ydir = Math.cos(lineAngle);
for (let i = 1; i < slices; i++) {
// move line perpendicular to angle
const dir = sideLength / slices * i * (i % 2 === 0 ? -1 : 1);
line[0].y += dir * xdir;
line[0].x -= dir * ydir;
line[1].y += dir * xdir;
line[1].x -= dir * ydir;
const result = this._tryDecode(line);
if (result) {
return result;
}
}
return null;
}
/**
* Used for development only
*/
private _printFrequency(line: Array<number>): void {
const context = this._frequencyCanvas.getContext('2d');
this._frequencyCanvas.width = line.length;
this._frequencyCanvas.height = 256;
context.beginPath();
context.strokeStyle = 'blue';
for (let i = 0; i < line.length; i++) {
context.moveTo(i, 255);
context.lineTo(i, 255 - line[i]);
}
context.closePath();
context.stroke();
}
/**
* Used for development only
*/
private _printPattern(line: Array<number>): void {
const context = this._patternCanvas.getContext('2d');
this._patternCanvas.width = line.length;
context.fillStyle = 'black';
for (let i = 0; i < line.length; i++) {
if (line[i] === 1) {
context.fillRect(i, 0, 1, 100);
}
}
}
private _drawPath(path: Array<Point>, color: string, lineWidth: number): void {
ImageDebug.drawPath(path, this._overlayContext, color, lineWidth);
}
}

@ -1,322 +0,0 @@
import Bresenham from './bresenham';
import ImageDebug from '../common/image_debug';
import Code128Reader from '../reader/code_128_reader';
import EANReader from '../reader/ean_reader';
import Code39Reader from '../reader/code_39_reader';
import Code39VINReader from '../reader/code_39_vin_reader';
import CodabarReader from '../reader/codabar_reader';
import UPCReader from '../reader/upc_reader';
import EAN8Reader from '../reader/ean_8_reader';
import EAN2Reader from '../reader/ean_2_reader';
import EAN5Reader from '../reader/ean_5_reader';
import UPCEReader from '../reader/upc_e_reader';
import I2of5Reader from '../reader/i2of5_reader';
import TwoOfFiveReader from '../reader/2of5_reader';
import Code93Reader from '../reader/code_93_reader';
const READERS = {
code_128_reader: Code128Reader,
ean_reader: EANReader,
ean_5_reader: EAN5Reader,
ean_2_reader: EAN2Reader,
ean_8_reader: EAN8Reader,
code_39_reader: Code39Reader,
code_39_vin_reader: Code39VINReader,
codabar_reader: CodabarReader,
upc_reader: UPCReader,
upc_e_reader: UPCEReader,
i2of5_reader: I2of5Reader,
'2of5_reader': TwoOfFiveReader,
code_93_reader: Code93Reader
};
export default {
create: function(config, inputImageWrapper) {
var _canvas = {
ctx: {
frequency: null,
pattern: null,
overlay: null
},
dom: {
frequency: null,
pattern: null,
overlay: null
}
},
_barcodeReaders = [];
initCanvas();
initReaders();
initConfig();
function initCanvas() {
if (ENV.development && typeof document !== 'undefined') {
var $debug = document.querySelector("#debug.detection");
_canvas.dom.frequency = document.querySelector("canvas.frequency");
if (!_canvas.dom.frequency) {
_canvas.dom.frequency = document.createElement("canvas");
_canvas.dom.frequency.className = "frequency";
if ($debug) {
$debug.appendChild(_canvas.dom.frequency);
}
}
_canvas.ctx.frequency = _canvas.dom.frequency.getContext("2d");
_canvas.dom.pattern = document.querySelector("canvas.patternBuffer");
if (!_canvas.dom.pattern) {
_canvas.dom.pattern = document.createElement("canvas");
_canvas.dom.pattern.className = "patternBuffer";
if ($debug) {
$debug.appendChild(_canvas.dom.pattern);
}
}
_canvas.ctx.pattern = _canvas.dom.pattern.getContext("2d");
_canvas.dom.overlay = document.querySelector("canvas.drawingBuffer");
if (_canvas.dom.overlay) {
_canvas.ctx.overlay = _canvas.dom.overlay.getContext("2d");
}
}
}
function initReaders() {
config.readers.forEach(function(readerConfig) {
var reader,
configuration = {},
supplements = [];
if (typeof readerConfig === 'object') {
reader = readerConfig.format;
configuration = readerConfig.config;
} else if (typeof readerConfig === 'string') {
reader = readerConfig;
}
if (ENV.development) {
console.log("Before registering reader: ", reader);
}
if (configuration.supplements) {
supplements = configuration
.supplements.map((supplement) => {
return new READERS[supplement]();
});
}
_barcodeReaders.push(new READERS[reader](configuration, supplements));
});
if (ENV.development) {
console.log("Registered Readers: " + _barcodeReaders
.map((reader) => JSON.stringify({format: reader.FORMAT, config: reader.config}))
.join(', '));
}
}
function initConfig() {
if (ENV.development && typeof document !== 'undefined') {
var i,
vis = [{
node: _canvas.dom.frequency,
prop: config.debug.showFrequency
}, {
node: _canvas.dom.pattern,
prop: config.debug.showPattern
}];
for (i = 0; i < vis.length; i++) {
if (vis[i].prop === true) {
vis[i].node.style.display = "block";
} else {
vis[i].node.style.display = "none";
}
}
}
}
/**
* extend the line on both ends
* @param {Array} line
* @param {Number} angle
*/
function getExtendedLine(line, angle, ext) {
function extendLine(amount) {
var extension = {
y: amount * Math.sin(angle),
x: amount * Math.cos(angle)
};
line[0].y -= extension.y;
line[0].x -= extension.x;
line[1].y += extension.y;
line[1].x += extension.x;
}
// check if inside image
extendLine(ext);
while (ext > 1 && (!inputImageWrapper.inImageWithBorder(line[0], 0)
|| !inputImageWrapper.inImageWithBorder(line[1], 0))) {
ext -= Math.ceil(ext / 2);
extendLine(-ext);
}
return line;
}
function getLine(box) {
return [{
x: (box[1][0] - box[0][0]) / 2 + box[0][0],
y: (box[1][1] - box[0][1]) / 2 + box[0][1]
}, {
x: (box[3][0] - box[2][0]) / 2 + box[2][0],
y: (box[3][1] - box[2][1]) / 2 + box[2][1]
}];
}
function tryDecode(line) {
var result = null,
i,
barcodeLine = Bresenham.getBarcodeLine(inputImageWrapper, line[0], line[1]);
if (ENV.development && config.debug.showFrequency) {
ImageDebug.drawPath(line, {x: 'x', y: 'y'}, _canvas.ctx.overlay, {color: 'red', lineWidth: 3});
Bresenham.debug.printFrequency(barcodeLine.line, _canvas.dom.frequency);
}
Bresenham.toBinaryLine(barcodeLine);
if (ENV.development && config.debug.showPattern) {
Bresenham.debug.printPattern(barcodeLine.line, _canvas.dom.pattern);
}
for ( i = 0; i < _barcodeReaders.length && result === null; i++) {
result = _barcodeReaders[i].decodePattern(barcodeLine.line);
}
if (result === null){
return null;
}
return {
codeResult: result,
barcodeLine: barcodeLine
};
}
/**
* This method slices the given area apart and tries to detect a barcode-pattern
* for each slice. It returns the decoded barcode, or null if nothing was found
* @param {Array} box
* @param {Array} line
* @param {Number} lineAngle
*/
function tryDecodeBruteForce(box, line, lineAngle) {
var sideLength = Math.sqrt(Math.pow(box[1][0] - box[0][0], 2) + Math.pow((box[1][1] - box[0][1]), 2)),
i,
slices = 16,
result = null,
dir,
extension,
xdir = Math.sin(lineAngle),
ydir = Math.cos(lineAngle);
for ( i = 1; i < slices && result === null; i++) {
// move line perpendicular to angle
dir = sideLength / slices * i * (i % 2 === 0 ? -1 : 1);
extension = {
y: dir * xdir,
x: dir * ydir
};
line[0].y += extension.x;
line[0].x -= extension.y;
line[1].y += extension.x;
line[1].x -= extension.y;
result = tryDecode(line);
}
return result;
}
function getLineLength(line) {
return Math.sqrt(
Math.pow(Math.abs(line[1].y - line[0].y), 2) +
Math.pow(Math.abs(line[1].x - line[0].x), 2));
}
/**
* With the help of the configured readers (Code128 or EAN) this function tries to detect a
* valid barcode pattern within the given area.
* @param {Object} box The area to search in
* @returns {Object} the result {codeResult, line, angle, pattern, threshold}
*/
function decodeFromBoundingBox(box) {
var line,
lineAngle,
ctx = _canvas.ctx.overlay,
result,
lineLength;
if (ENV.development) {
if (config.debug.drawBoundingBox && ctx) {
ImageDebug.drawPath(box, {x: 0, y: 1}, ctx, {color: "blue", lineWidth: 2});
}
}
line = getLine(box);
lineLength = getLineLength(line);
lineAngle = Math.atan2(line[1].y - line[0].y, line[1].x - line[0].x);
line = getExtendedLine(line, lineAngle, Math.floor(lineLength * 0.1));
if (line === null){
return null;
}
result = tryDecode(line);
if (result === null) {
result = tryDecodeBruteForce(box, line, lineAngle);
}
if (result === null) {
return null;
}
if (ENV.development && result && config.debug.drawScanline && ctx) {
ImageDebug.drawPath(line, {x: 'x', y: 'y'}, ctx, {color: 'red', lineWidth: 3});
}
return {
codeResult: result.codeResult,
line: line,
angle: lineAngle,
pattern: result.barcodeLine.line,
threshold: result.barcodeLine.threshold
};
}
return {
decodeFromBoundingBox: function(box) {
return decodeFromBoundingBox(box);
},
decodeFromBoundingBoxes: function(boxes) {
var i, result,
barcodes = [],
multiple = config.multiple;
for ( i = 0; i < boxes.length; i++) {
const box = boxes[i];
result = decodeFromBoundingBox(box) || {};
result.box = box;
if (multiple) {
barcodes.push(result);
} else if (result.codeResult) {
return result;
}
}
if (multiple) {
return {
barcodes
};
}
},
setReaders: function(readers) {
config.readers = readers;
_barcodeReaders.length = 0;
initReaders();
}
};
}
};

@ -1,198 +0,0 @@
import ImageWrapper from '../common/image_wrapper';
var Bresenham = {};
var Slope = {
DIR: {
UP: 1,
DOWN: -1
}
};
/**
* Scans a line of the given image from point p1 to p2 and returns a result object containing
* gray-scale values (0-255) of the underlying pixels in addition to the min
* and max values.
* @param {Object} imageWrapper
* @param {Object} p1 The start point {x,y}
* @param {Object} p2 The end point {x,y}
* @returns {line, min, max}
*/
Bresenham.getBarcodeLine = function(imageWrapper, p1, p2) {
var x0 = p1.x | 0,
y0 = p1.y | 0,
x1 = p2.x | 0,
y1 = p2.y | 0,
steep = Math.abs(y1 - y0) > Math.abs(x1 - x0),
deltax,
deltay,
error,
ystep,
y,
tmp,
x,
line = [],
imageData = imageWrapper.data,
width = imageWrapper.size.x,
sum = 0,
val,
min = 255,
max = 0;
function read(a, b) {
val = imageData[b * width + a];
sum += val;
min = val < min ? val : min;
max = val > max ? val : max;
line.push(val);
}
if (steep) {
tmp = x0;
x0 = y0;
y0 = tmp;
tmp = x1;
x1 = y1;
y1 = tmp;
}
if (x0 > x1) {
tmp = x0;
x0 = x1;
x1 = tmp;
tmp = y0;
y0 = y1;
y1 = tmp;
}
deltax = x1 - x0;
deltay = Math.abs(y1 - y0);
error = (deltax / 2) | 0;
y = y0;
ystep = y0 < y1 ? 1 : -1;
for ( x = x0; x < x1; x++) {
if (steep){
read(y, x);
} else {
read(x, y);
}
error = error - deltay;
if (error < 0) {
y = y + ystep;
error = error + deltax;
}
}
return {
line: line,
min: min,
max: max
};
};
/**
* Converts the result from getBarcodeLine into a binary representation
* also considering the frequency and slope of the signal for more robust results
* @param {Object} result {line, min, max}
*/
Bresenham.toBinaryLine = function(result) {
var min = result.min,
max = result.max,
line = result.line,
slope,
slope2,
center = min + (max - min) / 2,
extrema = [],
currentDir,
dir,
threshold = (max - min) / 12,
rThreshold = -threshold,
i,
j;
// 1. find extrema
currentDir = line[0] > center ? Slope.DIR.UP : Slope.DIR.DOWN;
extrema.push({
pos: 0,
val: line[0]
});
for ( i = 0; i < line.length - 2; i++) {
slope = (line[i + 1] - line[i]);
slope2 = (line[i + 2] - line[i + 1]);
if ((slope + slope2) < rThreshold && line[i + 1] < (center * 1.5)) {
dir = Slope.DIR.DOWN;
} else if ((slope + slope2) > threshold && line[i + 1] > (center * 0.5)) {
dir = Slope.DIR.UP;
} else {
dir = currentDir;
}
if (currentDir !== dir) {
extrema.push({
pos: i,
val: line[i]
});
currentDir = dir;
}
}
extrema.push({
pos: line.length,
val: line[line.length - 1]
});
for ( j = extrema[0].pos; j < extrema[1].pos; j++) {
line[j] = line[j] > center ? 0 : 1;
}
// iterate over extrema and convert to binary based on avg between minmax
for ( i = 1; i < extrema.length - 1; i++) {
if (extrema[i + 1].val > extrema[i].val) {
threshold = (extrema[i].val + ((extrema[i + 1].val - extrema[i].val) / 3) * 2) | 0;
} else {
threshold = (extrema[i + 1].val + ((extrema[i].val - extrema[i + 1].val) / 3)) | 0;
}
for ( j = extrema[i].pos; j < extrema[i + 1].pos; j++) {
line[j] = line[j] > threshold ? 0 : 1;
}
}
return {
line: line,
threshold: threshold
};
};
/**
* Used for development only
*/
Bresenham.debug = {
printFrequency: function(line, canvas) {
var i,
ctx = canvas.getContext("2d");
canvas.width = line.length;
canvas.height = 256;
ctx.beginPath();
ctx.strokeStyle = "blue";
for ( i = 0; i < line.length; i++) {
ctx.moveTo(i, 255);
ctx.lineTo(i, 255 - line[i]);
}
ctx.stroke();
ctx.closePath();
},
printPattern: function(line, canvas) {
var ctx = canvas.getContext("2d"), i;
canvas.width = line.length;
ctx.fillColor = "black";
for ( i = 0; i < line.length; i++) {
if (line[i] === 1) {
ctx.fillRect(i, 0, 1, 100);
}
}
}
};
export default Bresenham;

@ -0,0 +1,158 @@
import { Point } from '../common/point';
import { ImageWrapper } from '../common/image-wrapper';
enum Slope {
Up = 1,
Down = -1
};
export interface BarcodeLine {
line: Array<number>;
max?: number;
min?: number;
threshold?: number;
}
export const Bresenham = {
/**
* Scans a line of the given image from point p1 to p2 and returns a result object containing
* gray-scale values (0-255) of the underlying pixels in addition to the min and max values.
* @param imageWrapper
* @param p1 The start point {x,y}
* @param p2 The end point {x,y}
* @returns {line, min, max}
*/
getBarcodeLine(imageWrapper: ImageWrapper, p1: Point, p2: Point): BarcodeLine {
let x0 = p1.x | 0;
let y0 = p1.y | 0;
let x1 = p2.x | 0;
let y1 = p2.y | 0;
const steep = Math.abs(y1 - y0) > Math.abs(x1 - x0);
let tmp: number;
const line = [];
const imageData = imageWrapper.data;
const width = imageWrapper.size.x;
let val: number;
let min = 255;
let max = 0;
function read(a: number, b: number) {
val = imageData[b * width + a];
min = val < min ? val : min;
max = val > max ? val : max;
line.push(val);
}
if (steep) {
tmp = x0;
x0 = y0;
y0 = tmp;
tmp = x1;
x1 = y1;
y1 = tmp;
}
if (x0 > x1) {
tmp = x0;
x0 = x1;
x1 = tmp;
tmp = y0;
y0 = y1;
y1 = tmp;
}
let deltax = x1 - x0;
let deltay = Math.abs(y1 - y0);
let error = (deltax / 2) | 0;
let y = y0;
let ystep = y0 < y1 ? 1 : -1;
for (let x = x0; x < x1; x++) {
if (steep) {
read(y, x);
} else {
read(x, y);
}
error = error - deltay;
if (error < 0) {
y += ystep;
error = error + deltax;
}
}
return {
line,
min,
max
};
},
/**
* Converts the result from getBarcodeLine into a binary representation
* also considering the frequency and slope of the signal for more robust results
* @param result {line, min, max}
*/
toBinaryLine(result: BarcodeLine): BarcodeLine {
const min = result.min;
const max = result.max;
const line = result.line;
const center = min + (max - min) / 2;
const extrema = new Array<{ pos: number; val: number; }>();
let threshold = (max - min) / 12;
const rThreshold = -threshold;
// 1. find extrema
let currentDir = line[0] > center ? Slope.Up : Slope.Down;
extrema.push({
pos: 0,
val: line[0]
});
for (let i = 0; i < line.length - 2; i++) {
const slope = (line[i + 1] - line[i]);
const slope2 = (line[i + 2] - line[i + 1]);
let dir: Slope;
if ((slope + slope2) < rThreshold && line[i + 1] < (center * 1.5)) {
dir = Slope.Down;
} else if ((slope + slope2) > threshold && line[i + 1] > (center * 0.5)) {
dir = Slope.Up;
} else {
dir = currentDir;
}
if (currentDir !== dir) {
extrema.push({
pos: i,
val: line[i]
});
currentDir = dir;
}
}
extrema.push({
pos: line.length,
val: line[line.length - 1]
});
for (let j = extrema[0].pos; j < extrema[1].pos; j++) {
line[j] = line[j] > center ? 0 : 1;
}
// iterate over extrema and convert to binary based on avg between minmax
for (let i = 1; i < extrema.length - 1; i++) {
if (extrema[i + 1].val > extrema[i].val) {
threshold = (extrema[i].val + ((extrema[i + 1].val - extrema[i].val) / 3) * 2) | 0;
} else {
threshold = (extrema[i + 1].val + ((extrema[i].val - extrema[i + 1].val) / 3)) | 0;
}
for (let j = extrema[i].pos; j < extrema[i + 1].pos; j++) {
line[j] = line[j] > threshold ? 0 : 1;
}
}
return {
...result,
threshold
};
}
};

@ -0,0 +1,98 @@
import { getUserMedia, enumerateDevices } from '../common/media-devices';
let _stream: MediaStream;
export const CameraAccess = {
/**
* Attempts to attach the camera-stream to a given video element
* and calls the callback function when the content is ready
* @param video
* @param videoConstraints
*/
async request(video: HTMLVideoElement, videoConstraints: MediaTrackConstraints): Promise<void> {
const normalizedConstraints = CameraAccess.pickConstraints(videoConstraints);
_stream = await getUserMedia(normalizedConstraints);
video.srcObject = _stream;
video.setAttribute('autoplay', '');
video.setAttribute('muted', '');
video.setAttribute('playsinline', '');
return new Promise(resolve => video.addEventListener('loadedmetadata', () => {
video.play();
resolve();
})).then(_waitForVideo.bind(null, video));
},
release(): void {
const tracks = _stream && _stream.getVideoTracks();
if (tracks && tracks.length) {
tracks[0].stop();
}
_stream = null;
},
async enumerateVideoDevices(): Promise<Array<MediaDeviceInfo>> {
const devices = await enumerateDevices();
return devices.filter(({ kind }) => kind === 'videoinput');
},
getActiveStreamLabel(): string {
const track = CameraAccess.getActiveTrack();
return track ? track.label : '';
},
getActiveTrack() {
const tracks = _stream && _stream.getVideoTracks();
if (tracks && tracks.length) {
return tracks[0];
}
return null;
},
pickConstraints(videoConstraints: MediaTrackConstraints): MediaStreamConstraints {
let { width, height, facingMode, aspectRatio, deviceId } = videoConstraints;
const { minAspectRatio, facing } = videoConstraints as any;
if (typeof minAspectRatio !== 'undefined' && minAspectRatio > 0) {
aspectRatio = minAspectRatio;
console.log(`WARNING: Constraint 'minAspectRatio' is deprecated; Use 'aspectRatio' instead`);
}
if (typeof facing !== 'undefined') {
facingMode = facing;
console.log(`WARNING: Constraint 'facing' is deprecated. Use 'facingMode' instead'`);
}
const normalizedConstraints = deviceId && facingMode ?
{ width, height, aspectRatio, deviceId } : { width, height, facingMode, aspectRatio, deviceId };
return {
audio: false,
video: normalizedConstraints
};
}
}
function _waitForVideo({ videoWidth, videoHeight }): Promise<void> {
return new Promise((resolve, reject) => {
let attempts = 10;
function checkVideo() {
if (attempts > 0) {
if (videoWidth > 10 && videoHeight > 10) {
if (process.env.NODE_ENV !== 'production') {
console.log(`${videoWidth}px x ${videoHeight}px`);
}
resolve();
} else {
window.setTimeout(checkVideo, 500);
}
} else {
reject('Unable to play video stream. Is webcam working?');
}
attempts--;
}
checkVideo();
});
}

@ -1,119 +0,0 @@
import {omit, pick} from 'lodash';
import {getUserMedia, enumerateDevices} from 'mediaDevices';
const facingMatching = {
"user": /front/i,
"environment": /back/i
};
var streamRef;
function waitForVideo(video) {
return new Promise((resolve, reject) => {
let attempts = 10;
function checkVideo() {
if (attempts > 0) {
if (video.videoWidth > 10 && video.videoHeight > 10) {
if (ENV.development) {
console.log(video.videoWidth + "px x " + video.videoHeight + "px");
}
resolve();
} else {
window.setTimeout(checkVideo, 500);
}
} else {
reject('Unable to play video stream. Is webcam working?');
}
attempts--;
}
checkVideo();
});
}
/**
* Tries to attach the camera-stream to a given video-element
* and calls the callback function when the content is ready
* @param {Object} constraints
* @param {Object} video
*/
function initCamera(video, constraints) {
return getUserMedia(constraints)
.then((stream) => {
return new Promise((resolve) => {
streamRef = stream;
video.setAttribute("autoplay", true);
video.setAttribute('muted', true);
video.setAttribute('playsinline', true);
video.srcObject = stream;
video.addEventListener('loadedmetadata', () => {
video.play();
resolve();
});
});
})
.then(waitForVideo.bind(null, video));
}
function deprecatedConstraints(videoConstraints) {
const normalized = pick(videoConstraints, ["width", "height", "facingMode",
"aspectRatio", "deviceId"]);
if (typeof videoConstraints.minAspectRatio !== 'undefined' &&
videoConstraints.minAspectRatio > 0) {
normalized.aspectRatio = videoConstraints.minAspectRatio;
console.log("WARNING: Constraint 'minAspectRatio' is deprecated; Use 'aspectRatio' instead");
}
if (typeof videoConstraints.facing !== 'undefined') {
normalized.facingMode = videoConstraints.facing;
console.log("WARNING: Constraint 'facing' is deprecated. Use 'facingMode' instead'");
}
return normalized;
}
export function pickConstraints(videoConstraints) {
const normalizedConstraints = {
audio: false,
video: deprecatedConstraints(videoConstraints)
};
if (normalizedConstraints.video.deviceId
&& normalizedConstraints.video.facingMode) {
delete normalizedConstraints.video.facingMode;
}
return Promise.resolve(normalizedConstraints);
}
function enumerateVideoDevices() {
return enumerateDevices()
.then(devices => devices.filter(device => device.kind === 'videoinput'));
}
function getActiveTrack() {
if (streamRef) {
const tracks = streamRef.getVideoTracks();
if (tracks && tracks.length) {
return tracks[0];
}
}
}
export default {
request: function(video, videoConstraints) {
return pickConstraints(videoConstraints)
.then(initCamera.bind(null, video));
},
release: function() {
var tracks = streamRef && streamRef.getVideoTracks();
if (tracks && tracks.length) {
tracks[0].stop();
}
streamRef = null;
},
enumerateVideoDevices,
getActiveStreamLabel: function() {
const track = getActiveTrack();
return track ? track.label : '';
},
getActiveTrack
};

@ -0,0 +1,110 @@
/**
* @borrows https://github.com/exif-js/exif-js
*/
const ExifTags = { 0x0112: 'orientation' };
export const AvailableTags: Array<string> = Object.keys(ExifTags).map(key => ExifTags[key]);
export interface Tags {
[key: string]: number | string;
}
export async function findTagsInObjectURL(src: string, tags = AvailableTags): Promise<Tags> {
if (/^blob:/i.test(src)) {
const buffer = await objectURLToBlob(src);
return findTagsInBuffer(buffer, tags);
}
return Promise.resolve(null);
}
export function findTagsInBuffer(file: ArrayBuffer, selectedTags: Array<string> = AvailableTags): Tags {
const dataView = new DataView(file);
const length = file.byteLength;
const exifTags = selectedTags.reduce((result, selectedTag) => {
const exifTag = Object.keys(ExifTags).find(tag => ExifTags[tag] === selectedTag);
if (exifTag) {
result[exifTag] = selectedTag;
}
return result;
}, {});
let offset = 2;
if ((dataView.getUint8(0) !== 0xFF) || (dataView.getUint8(1) !== 0xD8)) {
return null;
}
while (offset < length) {
if (dataView.getUint8(offset) !== 0xFF) {
return null;
}
const marker = dataView.getUint8(offset + 1);
if (marker === 0xE1) {
return readEXIFData(dataView, offset + 4, exifTags);
} else {
offset += 2 + dataView.getUint16(offset + 2);
}
}
return null;
}
async function objectURLToBlob(url: string): Promise<ArrayBuffer> {
const response = await fetch(url);
if (response.ok) {
return response.arrayBuffer();
}
throw new Error('HTTP Error ' + response.status);
}
function readEXIFData(dataView: DataView, start: number, exifTags: { [key: number]: string }): Tags {
if ('Exif'.split('').some((char, index) => dataView.getUint8(start + index) !== char.charCodeAt(0))) {
return null;
}
const tiffOffset = start + 6;
let bigEnd: boolean;
if (dataView.getUint16(tiffOffset) === 0x4949) {
bigEnd = false;
} else if (dataView.getUint16(tiffOffset) === 0x4D4D) {
bigEnd = true;
} else {
return null;
}
if (dataView.getUint16(tiffOffset + 2, !bigEnd) !== 0x002A) {
return null;
}
const firstIFDOffset = dataView.getUint32(tiffOffset + 4, !bigEnd);
if (firstIFDOffset < 0x00000008) {
return null;
}
const tags = readTags(dataView, tiffOffset + firstIFDOffset, exifTags, bigEnd);
return tags;
}
function readTags(dataView: DataView, dirStart: number, strings: { [key: number]: string }, bigEnd: boolean): Tags {
const entries = dataView.getUint16(dirStart, !bigEnd);
const tags: Tags = {};
for (let i = 0; i < entries; i++) {
const entryOffset = dirStart + i * 12 + 2;
const tag = strings[dataView.getUint16(entryOffset, !bigEnd)];
if (tag) {
tags[tag] = readTagValue(dataView, entryOffset, bigEnd);
}
}
return tags;
}
function readTagValue(dataView: DataView, entryOffset: number, bigEnd: boolean): number | string {
const type = dataView.getUint16(entryOffset + 2, !bigEnd);
const numValues = dataView.getUint32(entryOffset + 4, !bigEnd);
return type === 3 && numValues === 1 ? dataView.getUint16(entryOffset + 8, !bigEnd) : undefined;
}

@ -1,146 +0,0 @@
// Scraped from https://github.com/exif-js/exif-js
const ExifTags = {0x0112: "orientation"};
export const AvailableTags = Object.keys(ExifTags).map(key => ExifTags[key]);
export function findTagsInObjectURL(src, tags = AvailableTags) {
if (/^blob\:/i.test(src)) {
return objectURLToBlob(src)
.then(readToBuffer)
.then(buffer => findTagsInBuffer(buffer, tags));
}
return Promise.resolve(null);
}
export function base64ToArrayBuffer(dataUrl) {
const base64 = dataUrl.replace(/^data\:([^\;]+)\;base64,/gmi, ''),
binary = atob(base64),
len = binary.length,
buffer = new ArrayBuffer(len),
view = new Uint8Array(buffer);
for (let i = 0; i < len; i++) {
view[i] = binary.charCodeAt(i);
}
return buffer;
}
function readToBuffer(blob) {
return new Promise(resolve => {
const fileReader = new FileReader();
fileReader.onload = function(e) {
return resolve(e.target.result);
};
fileReader.readAsArrayBuffer(blob);
});
}
function objectURLToBlob(url) {
return new Promise((resolve, reject) => {
const http = new XMLHttpRequest();
http.open("GET", url, true);
http.responseType = "blob";
http.onreadystatechange = function () {
if (http.readyState === XMLHttpRequest.DONE && (http.status === 200 || http.status === 0)) {
resolve(this.response);
}
};
http.onerror = reject;
http.send();
});
}
export function findTagsInBuffer(file, selectedTags = AvailableTags) {
const dataView = new DataView(file),
length = file.byteLength,
exifTags = selectedTags.reduce((result, selectedTag) => {
const exifTag = Object.keys(ExifTags).filter(tag => ExifTags[tag] === selectedTag)[0];
if (exifTag) {
result[exifTag] = selectedTag;
}
return result;
}, {});
let offset = 2,
marker;
if ((dataView.getUint8(0) !== 0xFF) || (dataView.getUint8(1) !== 0xD8)) {
return false;
}
while (offset < length) {
if (dataView.getUint8(offset) !== 0xFF) {
return false;
}
marker = dataView.getUint8(offset + 1);
if (marker === 0xE1) {
return readEXIFData(dataView, offset + 4, exifTags);
} else {
offset += 2 + dataView.getUint16(offset + 2);
}
}
}
function readEXIFData(file, start, exifTags) {
if (getStringFromBuffer(file, start, 4) !== "Exif") {
return false;
}
const tiffOffset = start + 6;
let bigEnd,
tags;
if (file.getUint16(tiffOffset) === 0x4949) {
bigEnd = false;
} else if (file.getUint16(tiffOffset) === 0x4D4D) {
bigEnd = true;
} else {
return false;
}
if (file.getUint16(tiffOffset + 2, !bigEnd) !== 0x002A) {
return false;
}
const firstIFDOffset = file.getUint32(tiffOffset + 4, !bigEnd);
if (firstIFDOffset < 0x00000008) {
return false;
}
tags = readTags(file, tiffOffset, tiffOffset + firstIFDOffset, exifTags, bigEnd);
return tags;
}
function readTags(file, tiffStart, dirStart, strings, bigEnd) {
const entries = file.getUint16(dirStart, !bigEnd),
tags = {};
for (let i = 0; i < entries; i++) {
const entryOffset = dirStart + i * 12 + 2,
tag = strings[file.getUint16(entryOffset, !bigEnd)];
if (tag) {
tags[tag] = readTagValue(file, entryOffset, tiffStart, dirStart, bigEnd);
}
}
return tags;
}
function readTagValue(file, entryOffset, tiffStart, dirStart, bigEnd) {
const type = file.getUint16(entryOffset + 2, !bigEnd),
numValues = file.getUint32(entryOffset + 4, !bigEnd);
switch (type) {
case 3:
if (numValues === 1) {
return file.getUint16(entryOffset + 8, !bigEnd);
}
}
}
function getStringFromBuffer(buffer, start, length) {
let outstr = "";
for (let n = start; n < start + length; n++) {
outstr += String.fromCharCode(buffer.getUint8(n));
}
return outstr;
}

@ -0,0 +1,129 @@
import { Point } from '../common/point';
import { InputStream } from './input-stream';
import { InputStreamConfig } from './input-stream-config';
const ndarray = require('ndarray');
type ndarray<_T = number> = any;
export class FrameGrabber {
private _inputStream: InputStream;
private _streamConfig: InputStreamConfig;
private _data: Uint8Array;
private _canvasData: Uint8Array;
private _grayData: Uint8Array;
private _canvasImageArray: ndarray<number>;
private _grayImageArray: ndarray<number>;
private _targetImageArray: ndarray<number>;
private _canvasHeight: number;
private _canvasWidth: number;
private _videoHeight: number;
private _videoWidth: number;
private _height: number;
private _width: number;
private _stepSizeX: number;
private _stepSizeY: number;
private _topLeft: Point;
constructor(inputStream: InputStream) {
this._inputStream = inputStream;
this._streamConfig = inputStream.config;
this._videoHeight = inputStream.realHeight;
this._videoWidth = inputStream.realWidth;
this._canvasHeight = inputStream.canvasHeight;
this._canvasWidth = inputStream.canvasWidth;
this._width = inputStream.width;
this._height = inputStream.height;
this._topLeft = inputStream.topLeft;
this._data = new Uint8Array(this._width * this._height);
this._grayData = new Uint8Array(this._videoWidth * this._videoHeight);
this._canvasData = new Uint8Array(this._canvasWidth * this._canvasHeight);
this._grayImageArray = ndarray(this._grayData, [this._videoHeight, this._videoWidth]).transpose(1, 0);
this._canvasImageArray = ndarray(this._canvasData, [this._canvasHeight, this._canvasWidth]).transpose(1, 0);
this._targetImageArray = this._canvasImageArray
.hi(this._topLeft.x + this._width, this._topLeft.y + this._height).lo(this._topLeft.x, this._topLeft.y);
this._stepSizeX = this._videoWidth / this._canvasWidth;
this._stepSizeY = this._videoHeight / this._canvasHeight;
console.log('FrameGrabber', JSON.stringify({
videoSize: this._grayImageArray.shape,
canvasSize: this._canvasImageArray.shape,
stepSize: [this._stepSizeX, this._stepSizeY],
size: this._targetImageArray.shape,
topLeft: this._topLeft
}));
}
/**
* Fetches a frame from the input stream and puts into the frame buffer.
* The image data is converted to gray scale and then half-sampled if configured.
*/
grab(data: Uint8Array): boolean {
this._data = data;
const frame = this._inputStream.getFrame();
if (frame) {
this._scaleAndCrop(frame);
return true;
} else {
return false;
}
}
private _scaleAndCrop(frame: ndarray<number>) {
// 1. compute full-sized gray image
this._computeGray(frame.data);
// 2. interpolate
for (let y = 0; y < this._canvasHeight; y++) {
for (let x = 0; x < this._canvasWidth; x++) {
this._canvasImageArray
.set(x, y, (interp2d(this._grayImageArray, x * this._stepSizeX, y * this._stepSizeY)) | 0);
}
}
// targetImageArray must be equal to targetSize
if (this._targetImageArray.shape[0] !== this._width || this._targetImageArray.shape[1] !== this._height) {
throw new Error('Shapes do not match!');
}
// 3. crop
for (let y = 0; y < this._height; y++) {
for (let x = 0; x < this._width; x++) {
this._data[y * this._width + x] = this._targetImageArray.get(x, y);
}
}
}
private _computeGray(imageData: Uint8ClampedArray): void {
const imageDataLength = imageData.length;
if (this._streamConfig && this._streamConfig.singleChannel) {
for (let i = 0, j = 0; i < imageDataLength; i += 4, j++) {
this._data[j] = imageData[i];
}
} else {
for (let i = 0, j = 0; i < imageDataLength; i += 4, j++) {
this._data[j] = 0.299 * imageData[i] + 0.587 * imageData[i + 1] + 0.114 * imageData[i + 2] | 0;
}
}
}
}
/**
* @borrows https://github.com/scijs/ndarray-linear-interpolate
*/
function interp2d(arr: ndarray<number>, x: number, y: number): number {
const ix = Math.floor(x);
const fx = x - ix;
const s0 = 0 <= ix && ix < arr.shape[0];
const s1 = 0 <= ix + 1 && ix + 1 < arr.shape[0];
const iy = Math.floor(y);
const fy = y - iy;
const t0 = 0 <= iy && iy < arr.shape[1];
const t1 = 0 <= iy + 1 && iy + 1 < arr.shape[1];
const w00 = s0 && t0 ? arr.get(ix, iy) : 0.0;
const w01 = s0 && t1 ? arr.get(ix, iy + 1) : 0.0;
const w10 = s1 && t0 ? arr.get(ix + 1, iy) : 0.0;
const w11 = s1 && t1 ? arr.get(ix + 1, iy + 1) : 0.0;
return (1.0 - fy) * ((1.0 - fx) * w00 + fx * w10) + fy * ((1.0 - fx) * w01 + fx * w11);
}

@ -0,0 +1,161 @@
import { Point } from '../common/point';
import { InputStream } from './input-stream';
import { InputStreamConfig } from './input-stream-config';
const QUATER_CIRCLE = Math.PI / 2;
export class FrameGrabber {
private _inputStream: InputStream;
private _streamConfig: InputStreamConfig;
private _canvas: HTMLCanvasElement;
private _context: CanvasRenderingContext2D;
private _data: Uint8Array;
private _canvasHeight: number;
private _canvasWidth: number;
private _height: number;
private _width: number;
private _topLeft: Point;
constructor(inputStream: InputStream, canvas: HTMLCanvasElement) {
this._inputStream = inputStream;
this._streamConfig = inputStream.config;
this._canvasWidth = inputStream.canvasWidth;
this._canvasHeight = inputStream.canvasHeight;
this._width = inputStream.width;
this._height = inputStream.height;
this._topLeft = inputStream.topLeft;
this._canvas = canvas || document.createElement('canvas');
this._canvas.width = this._canvasWidth;
this._canvas.height = this._canvasHeight;
this._context = this._canvas.getContext('2d');
this._data = new Uint8Array(this._width * this._height);
if (process.env.NODE_ENV !== 'production') {
console.log('FrameGrabber', JSON.stringify({
size: {
x: this._width,
y: this._height
},
topLeft: this._topLeft,
videoSize: {
x: inputStream.realWidth,
y: inputStream.realHeight
},
canvasSize: {
x: this._canvasWidth,
y: this._canvasHeight
}
}));
}
}
/**
* Fetches a frame from the input stream and puts into the frame buffer.
* The image data is converted to gray scale and then half-sampled if configured.
*/
grab(data: Uint8Array): boolean {
this._data = data;
const frame = this._inputStream.getFrame();
if (frame) {
this._adjustCanvasSize();
let drawable: HTMLImageElement | HTMLVideoElement;
let drawAngle = 0;
if (frame instanceof HTMLVideoElement) {
drawable = frame;
} else {
drawable = frame.image;
if (frame.tags) {
switch (frame.tags.orientation) {
case 6: {
drawAngle = QUATER_CIRCLE;
break;
}
case 8: {
drawAngle = -QUATER_CIRCLE;
break;
}
}
}
}
if (drawAngle !== 0) {
const halfWidth = this._canvasWidth >> 1;
const halfHeight = this._canvasHeight >> 1;
this._context.translate(halfWidth, halfHeight);
this._context.rotate(drawAngle);
this._context.drawImage(drawable, -halfHeight, -halfWidth, this._canvasHeight, this._canvasWidth);
this._context.rotate(-drawAngle);
this._context.translate(-halfWidth, -halfHeight);
} else {
this._context.drawImage(drawable, 0, 0, this._canvasWidth, this._canvasHeight);
}
const imageData = this._context.getImageData(this._topLeft.x, this._topLeft.y, this._width, this._height).data;
if (this._streamConfig.halfSample) {
this._grayAndHalfSampleFromCanvasData(imageData);
} else {
this._computeGray(imageData);
}
return true;
} else {
return false;
}
}
private _adjustCanvasSize(): void {
if (this._canvas.height !== this._canvasHeight || this._canvas.width !== this._canvasWidth) {
if (process.env.NODE_ENV !== 'production') {
console.warn('Canvas size needs to be adjusted');
}
this._canvas.height = this._canvasHeight;
this._canvas.width = this._canvasWidth;
}
}
private _grayAndHalfSampleFromCanvasData(imageData: Uint8ClampedArray): void {
const endIndex = imageData.length >> 2;
const outWidth = this._width >> 1;
let topRowIndex = 0;
let bottomRowIndex = this._width;
let outImageIndex = 0;
while (bottomRowIndex < endIndex) {
for (let i = 0; i < outWidth; i++) {
const top4 = topRowIndex << 2;
const bottom4 = bottomRowIndex << 2;
this._data[outImageIndex] = (
(0.299 * imageData[top4 + 0] + 0.587 * imageData[top4 + 1] + 0.114 * imageData[top4 + 2]) +
(0.299 * imageData[top4 + 4] + 0.587 * imageData[top4 + 5] + 0.114 * imageData[top4 + 6]) +
(0.299 * imageData[bottom4 + 0] + 0.587 * imageData[bottom4 + 1] + 0.114 * imageData[bottom4 + 2]) +
(0.299 * imageData[bottom4 + 4] + 0.587 * imageData[bottom4 + 5] + 0.114 * imageData[bottom4 + 6])
) / 4 | 0;
outImageIndex++;
topRowIndex += 2;
bottomRowIndex += 2;
}
topRowIndex += this._width;
bottomRowIndex += this._width;
}
}
private _computeGray(imageData: Uint8ClampedArray): void {
const imageDataLength = imageData.length;
if (this._streamConfig && this._streamConfig.singleChannel) {
for (let i = 0, j = 0; i < imageDataLength; i += 4, j++) {
this._data[j] = imageData[i];
}
} else {
for (let i = 0, j = 0; i < imageDataLength; i += 4, j++) {
this._data[j] = 0.299 * imageData[i] + 0.587 * imageData[i + 1] + 0.114 * imageData[i + 2] | 0;
}
}
}
}

@ -1,122 +0,0 @@
import {
imageRef,
grayAndHalfSampleFromCanvasData,
computeGray
} from '../common/cv_utils';
const TO_RADIANS = Math.PI / 180;
function adjustCanvasSize(canvas, targetSize) {
if (canvas.width !== targetSize.x) {
if (ENV.development) {
console.log("WARNING: canvas-size needs to be adjusted");
}
canvas.width = targetSize.x;
}
if (canvas.height !== targetSize.y) {
if (ENV.development) {
console.log("WARNING: canvas-size needs to be adjusted");
}
canvas.height = targetSize.y;
}
}
var FrameGrabber = {};
FrameGrabber.create = function(inputStream, canvas) {
var _that = {},
_streamConfig = inputStream.getConfig(),
_video_size = imageRef(inputStream.getRealWidth(), inputStream.getRealHeight()),
_canvasSize = inputStream.getCanvasSize(),
_size = imageRef(inputStream.getWidth(), inputStream.getHeight()),
topRight = inputStream.getTopRight(),
_sx = topRight.x,
_sy = topRight.y,
_canvas,
_ctx = null,
_data = null;
_canvas = canvas ? canvas : document.createElement("canvas");
_canvas.width = _canvasSize.x;
_canvas.height = _canvasSize.y;
_ctx = _canvas.getContext("2d");
_data = new Uint8Array(_size.x * _size.y);
if (ENV.development) {
console.log("FrameGrabber", JSON.stringify({
size: _size,
topRight: topRight,
videoSize: _video_size,
canvasSize: _canvasSize
}));
}
/**
* Uses the given array as frame-buffer
*/
_that.attachData = function(data) {
_data = data;
};
/**
* Returns the used frame-buffer
*/
_that.getData = function() {
return _data;
};
/**
* Fetches a frame from the input-stream and puts into the frame-buffer.
* The image-data is converted to gray-scale and then half-sampled if configured.
*/
_that.grab = function() {
var doHalfSample = _streamConfig.halfSample,
frame = inputStream.getFrame(),
drawable = frame,
drawAngle = 0,
ctxData;
if (drawable) {
adjustCanvasSize(_canvas, _canvasSize);
if (_streamConfig.type === 'ImageStream') {
drawable = frame.img;
if (frame.tags && frame.tags.orientation) {
switch (frame.tags.orientation) {
case 6:
drawAngle = 90 * TO_RADIANS;
break;
case 8:
drawAngle = -90 * TO_RADIANS;
break;
}
}
}
if (drawAngle !== 0) {
_ctx.translate(_canvasSize.x / 2, _canvasSize.y / 2);
_ctx.rotate(drawAngle);
_ctx.drawImage(drawable, -_canvasSize.y / 2, -_canvasSize.x / 2, _canvasSize.y, _canvasSize.x);
_ctx.rotate(-drawAngle);
_ctx.translate(-_canvasSize.x / 2, -_canvasSize.y / 2);
} else {
_ctx.drawImage(drawable, 0, 0, _canvasSize.x, _canvasSize.y);
}
ctxData = _ctx.getImageData(_sx, _sy, _size.x, _size.y).data;
if (doHalfSample){
grayAndHalfSampleFromCanvasData(ctxData, _size, _data);
} else {
computeGray(ctxData, _data, _streamConfig);
}
return true;
} else {
return false;
}
};
_that.getSize = function() {
return _size;
};
return _that;
};
export default FrameGrabber;

@ -0,0 +1,67 @@
import { findTagsInObjectURL, Tags } from './exif-helper';
export interface ImageInfo {
image: HTMLImageElement;
tags?: Tags;
}
export class ImageLoader {
static async load(
baseUri: string,
callback: (_: Array<ImageInfo>) => void,
offset: number,
size: number,
sequence: boolean
): Promise<void> {
const imageSrcs = new Array<string>(size);
const loadedImages = new Array<ImageInfo>(size);
const notLoadedImages = new Array<HTMLImageElement>();
if (sequence === false) {
imageSrcs[0] = baseUri;
} else {
for (let i = 0; i < size; i++) {
imageSrcs[i] = `${baseUri}image-${('00' + (offset + i)).slice(-3)}.jpg`;
}
}
imageSrcs.forEach(src => {
const image = new Image();
notLoadedImages.push(image);
image.onload = () => loaded(image);
image.src = src;
});
async function loaded(loadedImage: HTMLImageElement): Promise<void> {
for (let x = 0; x < notLoadedImages.length; x++) {
if (notLoadedImages[x] === loadedImage) {
notLoadedImages.splice(x, 1);
// TODO: assume the index is the same
for (let y = 0; y < imageSrcs.length; y++) {
const imageName = imageSrcs[y].substr(imageSrcs[y].lastIndexOf('/'));
if (loadedImage.src.lastIndexOf(imageName) !== -1) {
loadedImages[y] = { image: loadedImage };
break;
}
}
break;
}
}
if (notLoadedImages.length === 0) {
if (process.env.NODE_ENV !== 'production') {
console.log('Images loaded');
}
try {
if (sequence === false) {
const firstImage = loadedImages[0];
firstImage.tags = await findTagsInObjectURL(baseUri);
}
} catch (ex) {
console.log(ex);
} finally {
callback(loadedImages);
}
}
}
}
}

@ -0,0 +1,96 @@
import { ImageInfo } from './image-loader';
import { InputStream } from './input-stream';
import { InputStreamConfig } from './input-stream-config';
const getPixels = require('get-pixels');
type ndarray<_T = number> = any;
export class ImageStream extends InputStream {
private _baseUrl: string;
private _ended: boolean;
private _frame: ndarray<number>;
private _height: number;
private _loaded: boolean;
private _offset: number;
private _size: number;
private _width: number;
constructor() {
super();
this._canvasHeight = 0;
this._canvasWidth = 0;
this._baseUrl = null;
this._ended = false;
this._frame = null;
this._height = 0;
this._loaded = false;
this._offset = 1;
this._size = 0;
this._width = 0;
}
get realHeight(): number {
return this._height;
}
get realWidth(): number {
return this._width;
}
get config(): InputStreamConfig {
return this._config;
}
set config(config: InputStreamConfig) {
this._config = { ...config };
this._baseUrl = config.src || '';
this._loadImages();
}
get ended(): boolean {
return this._ended;
}
setAttribute() { }
pause(): void { }
play(): void { }
set currentTime(_time: number) { }
getFrame(): HTMLVideoElement | ImageInfo {
if (!this._loaded) {
return null;
}
return this._frame as any;
}
private _loadImages(): void {
this._loaded = false;
getPixels(this._baseUrl, this._config.mime, (err: any, pixels: ndarray<number>) => {
if (err) {
console.log(err);
process.exit(1);
}
this._loaded = true;
this._frame = pixels;
console.log(pixels.shape);
this._width = pixels.shape[0] | 0;
this._height = pixels.shape[1] | 0;
this._canvasWidth = this._calculatedWidth = this._config.size ? this._width > this._height ?
this._config.size : this._width * this._config.size / this._height | 0 : this._width;
this._canvasHeight = this._calculatedHeight = this._config.size ? this._width > this._height ?
this._height * this._config.size / this._width | 0 : this._config.size : this._height;
setTimeout(() => this.trigger('canrecord', []), 0);
}, this._offset, this._size, this._config.sequence);
}
}

@ -0,0 +1,118 @@
import { ImageInfo, ImageLoader } from './image-loader';
import { InputStream } from './input-stream';
import { InputStreamConfig } from './input-stream-config';
export class ImageStream extends InputStream {
private _baseUrl: string;
private _ended: boolean;
private _frameIndex: number;
private _height: number;
private _images: Array<ImageInfo>;
private _loaded: boolean;
private _offset: number;
private _paused: boolean;
private _size: number;
private _width: number;
constructor() {
super();
this._canvasHeight = 0;
this._canvasWidth = 0;
this._baseUrl = null;
this._ended = false;
this._frameIndex = 0;
this._height = 0;
this._images = null;
this._loaded = false;
this._offset = 1;
this._paused = true;
this._size = 0;
this._width = 0;
}
get realHeight(): number {
return this._height;
}
get realWidth(): number {
return this._width;
}
get config(): InputStreamConfig {
return this._config;
}
set config(config: InputStreamConfig) {
this._config = { ...config };
this._baseUrl = config.src;
this._size = config.sequence && config.length ? config.length : 1;
this._loadImages();
}
get ended(): boolean {
return this._ended;
}
setAttribute() { }
pause(): void {
this._paused = true;
}
play(): void {
this._paused = false;
}
set currentTime(time: number) {
this._frameIndex = time;
}
getFrame(): HTMLVideoElement | ImageInfo {
let frame: ImageInfo = null;
if (this._loaded && !this._paused) {
frame = this._images[this._frameIndex];
if (this._frameIndex < (this._size - 1)) {
this._frameIndex++;
} else {
setTimeout(() => {
this._ended = true;
this.trigger('ended', []);
}, 0);
}
}
return frame;
}
private _loadImages(): void {
this._loaded = false;
ImageLoader.load(this._baseUrl, images => {
this._images = images;
switch (images[0].tags && images[0].tags.orientation) {
case 6:
case 8: {
this._width = images[0].image.height;
this._height = images[0].image.width;
break;
}
default: {
this._width = images[0].image.width;
this._height = images[0].image.height;
}
}
this._canvasWidth = this._calculatedWidth = this._config.size ? this._width > this._height ?
this._config.size : this._width * this._config.size / this._height | 0 : this._width;
this._canvasHeight = this._calculatedHeight = this._config.size ? this._width > this._height ?
this._height * this._config.size / this._width | 0 : this._config.size : this._height;
this._loaded = true;
this._frameIndex = 0;
setTimeout(() => this.trigger('canrecord', []), 0);
}, this._offset, this._size, this._config.sequence);
}
}

@ -1,71 +0,0 @@
import {findTagsInObjectURL} from './exif_helper';
var ImageLoader = {};
ImageLoader.load = function(directory, callback, offset, size, sequence) {
var htmlImagesSrcArray = new Array(size),
htmlImagesArray = new Array(htmlImagesSrcArray.length),
i,
img,
num;
if (sequence === false) {
htmlImagesSrcArray[0] = directory;
} else {
for ( i = 0; i < htmlImagesSrcArray.length; i++) {
num = (offset + i);
htmlImagesSrcArray[i] = directory + "image-" + ("00" + num).slice(-3) + ".jpg";
}
}
htmlImagesArray.notLoaded = [];
htmlImagesArray.addImage = function(image) {
htmlImagesArray.notLoaded.push(image);
};
htmlImagesArray.loaded = function(loadedImg) {
var notloadedImgs = htmlImagesArray.notLoaded;
for (var x = 0; x < notloadedImgs.length; x++) {
if (notloadedImgs[x] === loadedImg) {
notloadedImgs.splice(x, 1);
for (var y = 0; y < htmlImagesSrcArray.length; y++) {
var imgName = htmlImagesSrcArray[y].substr(htmlImagesSrcArray[y].lastIndexOf("/"));
if (loadedImg.src.lastIndexOf(imgName) !== -1) {
htmlImagesArray[y] = {img: loadedImg};
break;
}
}
break;
}
}
if (notloadedImgs.length === 0) {
if (ENV.development) {
console.log("Images loaded");
}
if (sequence === false) {
findTagsInObjectURL(directory, ['orientation'])
.then(tags => {
htmlImagesArray[0].tags = tags;
callback(htmlImagesArray);
}).catch(e => {
console.log(e);
callback(htmlImagesArray);
});
} else {
callback(htmlImagesArray);
}
}
};
for ( i = 0; i < htmlImagesSrcArray.length; i++) {
img = new Image();
htmlImagesArray.addImage(img);
addOnloadHandler(img, htmlImagesArray);
img.src = htmlImagesSrcArray[i];
}
};
function addOnloadHandler(img, htmlImagesArray) {
img.onload = function() {
htmlImagesArray.loaded(this);
};
}
export default (ImageLoader);

@ -0,0 +1,23 @@
type InputStreamType = 'ImageStream' | 'LiveStream' | 'VideoStream';
export interface AreaConfig {
bottom?: string;
left?: string;
right?: string;
top?: string;
}
export interface InputStreamConfig {
area?: AreaConfig;
constraints?: MediaTrackConstraints;
halfSample?: boolean;
length?: number;
mime?: string;
name?: string;
sequence?: boolean;
singleChannel?: boolean;
size?: number;
src?: string;
target?: HTMLElement | string;
type?: InputStreamType;
}

@ -0,0 +1,169 @@
import { Point } from '../common/point';
import { BarcodeLocatorConfig, PatchSizeConfig } from '../locator/barcode-locator-config';
import { InputStream } from './input-stream';
import { AreaConfig } from './input-stream-config';
export interface Dimension {
unit: '%' | 'px',
value: number;
}
function _computeDivisors(n: number): Array<number> {
const divisors = new Array<number>();
const largeDivisors = new Array<number>();
for (let divisor = 1; divisor * divisor <= n; divisor++) {
if (n % divisor === 0) {
divisors.push(divisor);
if (divisor * divisor !== n) {
largeDivisors.unshift(n / divisor | 0);
}
}
}
return divisors.concat(largeDivisors);
}
function _computeCommonDivisors(m: number, n: number): Array<number> {
if (m === n) {
return _computeDivisors(m);
}
const max = m > n ? m : n;
const min = m > n ? n : m;
const divisors = new Array<number>();
const largeDivisors = new Array<number>();
for (let divisor = 1; divisor * divisor <= min; divisor++) {
if (max % divisor === 0 && min % divisor === 0) {
divisors.push(divisor);
const largeDivisor = min / divisor | 0;
if (divisor !== largeDivisor && max % largeDivisor === 0) {
largeDivisors.unshift();
}
}
}
return divisors.concat(largeDivisors);
}
export function calculatePatchSize(patchSize: PatchSizeConfig, { x, y }: Point): Point {
const wideSide = Math.max(x | 0, y | 0) | 0;
const nrOfPatchesList = [8, 10, 15, 20, 32, 60, 80];
const nrOfPatchesMap = {
'x-small': 5,
small: 4,
medium: 3,
large: 2,
'x-large': 1
};
const nrOfPatchesIndex = nrOfPatchesMap[patchSize] || nrOfPatchesMap.medium | 0;
const nrOfPatches = nrOfPatchesList[nrOfPatchesIndex] | 0;
const desiredPatchSize = wideSide / nrOfPatches | 0;
function findPatchSizeForDivisors(divisors: Array<number>): Point {
let i = 0;
let found = divisors[divisors.length >> 1] | 0;
while (i < (divisors.length - 1) && divisors[i] < desiredPatchSize) {
i++;
}
if (i > 0) {
if (Math.abs(divisors[i] - desiredPatchSize) > Math.abs(divisors[i - 1] - desiredPatchSize)) {
found = divisors[i - 1] | 0;
} else {
found = divisors[i] | 0;
}
}
if (desiredPatchSize / found < nrOfPatchesList[nrOfPatchesIndex + 1] / nrOfPatchesList[nrOfPatchesIndex] &&
desiredPatchSize / found > nrOfPatchesList[nrOfPatchesIndex - 1] / nrOfPatchesList[nrOfPatchesIndex]) {
return { x: found, y: found };
}
return null;
}
const optimalPatchSize = findPatchSizeForDivisors(_computeCommonDivisors(x, y)) ||
findPatchSizeForDivisors(_computeDivisors(wideSide)) ||
findPatchSizeForDivisors(_computeDivisors(desiredPatchSize * nrOfPatches));
return optimalPatchSize;
}
export function checkImageConstraints(inputStream: InputStream, config: BarcodeLocatorConfig) {
let width = inputStream.width;
let height = inputStream.height;
const shift = config.halfSample ? 1 : 0 | 0;
const inputStreamConfig = inputStream.config;
// calculate width and height based on area
if (inputStreamConfig && inputStreamConfig.area) {
const area = computeImageArea(width, height, inputStreamConfig.area);
inputStream.topLeft = area.topLeft;
inputStream.setCanvasSize(width, height);
width = area.width;
height = area.height;
}
const size = {
x: width >> shift,
y: height >> shift
};
const patchSize = calculatePatchSize(config.patchSize, size);
if (process.env.NODE_ENV !== 'production') {
console.log('Patch-Size:', JSON.stringify(patchSize));
}
inputStream.width = (size.x / patchSize.x << shift) * patchSize.x | 0;
inputStream.height = (size.y / patchSize.y << shift) * patchSize.y | 0;
if ((inputStream.width % patchSize.x) === 0 && (inputStream.height % patchSize.y) === 0) {
return true;
}
// eslint-disable-next-line max-len
throw new Error(`Image dimensions do not comply with the current settings: width (${width}) and height (${height}) must be a multiple of ${patchSize.x}`);
}
export function _parseCssDimensionValues(value: string): Dimension {
const dimension: Dimension = {
value: parseFloat(value),
unit: value.indexOf('%') === value.length - 1 ? '%' : value.indexOf('px') === value.length - 2 ? 'px' : '%'
};
return dimension;
}
export const _dimensionsConverters = {
bottom: (dimension: Dimension, { height }) => dimension.unit === '%' ?
height - height * dimension.value / 100 | 0 : dimension.unit === 'px' ? height - dimension.value : height,
left: (dimension: Dimension, { width }) => dimension.unit === '%' ?
width * dimension.value / 100 | 0 : dimension.unit === 'px' ? dimension.value : 0,
right: (dimension: Dimension, { width }) => dimension.unit === '%' ?
width - width * dimension.value / 100 | 0 : dimension.unit === 'px' ? width - dimension.value : width,
top: (dimension: Dimension, { height }): number => dimension.unit === '%' ?
height * dimension.value / 100 | 0 : dimension.unit === 'px' ? dimension.value : 0
};
export function computeImageArea(inputWidth: number, inputHeight: number, area: AreaConfig) {
const context = { width: inputWidth, height: inputHeight };
const parsedArea: {
bottom?: number;
left?: number;
right?: number;
top?: number;
} = Object.keys(area).reduce((result, key) => {
const value = area[key];
const parsed = _parseCssDimensionValues(value);
const calculated = _dimensionsConverters[key](parsed, context);
result[key] = calculated;
return result;
}, {});
return {
topLeft: { x: parsedArea.left, y: parsedArea.top },
width: parsedArea.right - parsedArea.left,
height: parsedArea.bottom - parsedArea.top
};
}

@ -0,0 +1,102 @@
import { Point } from '../common/point';
import { ImageInfo } from './image-loader';
import { InputStreamConfig } from './input-stream-config';
export abstract class InputStream {
protected _calculatedHeight: number;
protected _calculatedWidth: number;
protected _canvasHeight: number;
protected _canvasWidth: number;
protected _config: InputStreamConfig;
protected _eventNames: Array<string>;
protected _eventHandlers: Map<string, Array<EventListener>>;
protected _topLeft: Point;
constructor() {
this._canvasWidth = 0;
this._canvasHeight = 0;
this._config = null;
this._eventNames = ['canrecord', 'ended'];
this._eventHandlers = new Map<string, Array<EventListener>>();
this._topLeft = { x: 0, y: 0 };
}
abstract get realHeight(): number;
abstract get realWidth(): number;
get height(): number {
return this._calculatedHeight;
}
set height(height: number) {
this._calculatedHeight = height;
}
get width(): number {
return this._calculatedWidth;
}
set width(width: number) {
this._calculatedWidth = width;
}
get topLeft(): Point {
return { ...this._topLeft };
}
set topLeft(topLeft: Point) {
this._topLeft.x = topLeft.x;
this._topLeft.y = topLeft.y;
}
setCanvasSize(width: number, height: number): void {
this._canvasWidth = width;
this._canvasHeight = height;
}
get canvasHeight(): number {
return this._canvasHeight;
}
get canvasWidth(): number {
return this._canvasWidth;
}
abstract get config(): InputStreamConfig;
abstract set config(config: InputStreamConfig);
abstract get ended(): boolean;
abstract setAttribute(name: string, value: string): void;
abstract pause(): void;
abstract play(): void;
abstract set currentTime(time: number);
addEventListener(event: string, listener: EventListener, _options?: boolean): void {
if (this._eventNames.indexOf(event) !== -1) {
if (!this._eventHandlers.has(event)) {
this._eventHandlers.set(event, new Array<EventListener>());
}
this._eventHandlers.get(event).push(listener);
}
}
clearEventHandlers(): void {
this._eventHandlers.clear();
}
trigger(eventName: string, argArray?: any) {
const handlers = this._eventHandlers.get(eventName);
if (handlers) {
handlers.forEach(handler => handler.apply(this, argArray));
}
}
abstract getFrame(): HTMLVideoElement | ImageInfo;
}

@ -1,330 +0,0 @@
import ImageLoader from './image_loader';
var InputStream = {};
InputStream.createVideoStream = function(video) {
var that = {},
_config = null,
_eventNames = ['canrecord', 'ended'],
_eventHandlers = {},
_calculatedWidth,
_calculatedHeight,
_topRight = {x: 0, y: 0},
_canvasSize = {x: 0, y: 0};
function initSize() {
var width = video.videoWidth,
height = video.videoHeight;
_calculatedWidth =
_config.size ? width / height > 1 ? _config.size : Math.floor((width / height) * _config.size) : width;
_calculatedHeight =
_config.size ? width / height > 1 ? Math.floor((height / width) * _config.size) : _config.size : height;
_canvasSize.x = _calculatedWidth;
_canvasSize.y = _calculatedHeight;
}
that.getRealWidth = function() {
return video.videoWidth;
};
that.getRealHeight = function() {
return video.videoHeight;
};
that.getWidth = function() {
return _calculatedWidth;
};
that.getHeight = function() {
return _calculatedHeight;
};
that.setWidth = function(width) {
_calculatedWidth = width;
};
that.setHeight = function(height) {
_calculatedHeight = height;
};
that.setInputStream = function(config) {
_config = config;
video.src = (typeof config.src !== 'undefined') ? config.src : '';
};
that.ended = function() {
return video.ended;
};
that.getConfig = function() {
return _config;
};
that.setAttribute = function(name, value) {
video.setAttribute(name, value);
};
that.pause = function() {
video.pause();
};
that.play = function() {
video.play();
};
that.setCurrentTime = function(time) {
if (_config.type !== "LiveStream") {
video.currentTime = time;
}
};
that.addEventListener = function(event, f, bool) {
if (_eventNames.indexOf(event) !== -1) {
if (!_eventHandlers[event]) {
_eventHandlers[event] = [];
}
_eventHandlers[event].push(f);
} else {
video.addEventListener(event, f, bool);
}
};
that.clearEventHandlers = function() {
_eventNames.forEach(function(eventName) {
var handlers = _eventHandlers[eventName];
if (handlers && handlers.length > 0) {
handlers.forEach(function(handler) {
video.removeEventListener(eventName, handler);
});
}
});
};
that.trigger = function(eventName, args) {
var j,
handlers = _eventHandlers[eventName];
if (eventName === 'canrecord') {
initSize();
}
if (handlers && handlers.length > 0) {
for ( j = 0; j < handlers.length; j++) {
handlers[j].apply(that, args);
}
}
};
that.setTopRight = function(topRight) {
_topRight.x = topRight.x;
_topRight.y = topRight.y;
};
that.getTopRight = function() {
return _topRight;
};
that.setCanvasSize = function(size) {
_canvasSize.x = size.x;
_canvasSize.y = size.y;
};
that.getCanvasSize = function() {
return _canvasSize;
};
that.getFrame = function() {
return video;
};
return that;
};
InputStream.createLiveStream = function(video) {
video.setAttribute("autoplay", true);
var that = InputStream.createVideoStream(video);
that.ended = function() {
return false;
};
return that;
};
InputStream.createImageStream = function() {
var that = {};
var _config = null;
var width = 0,
height = 0,
frameIdx = 0,
paused = true,
loaded = false,
imgArray = null,
size = 0,
offset = 1,
baseUrl = null,
ended = false,
calculatedWidth,
calculatedHeight,
_eventNames = ['canrecord', 'ended'],
_eventHandlers = {},
_topRight = {x: 0, y: 0},
_canvasSize = {x: 0, y: 0};
function loadImages() {
loaded = false;
ImageLoader.load(baseUrl, function(imgs) {
imgArray = imgs;
if (imgs[0].tags && imgs[0].tags.orientation) {
switch (imgs[0].tags.orientation) {
case 6:
case 8:
width = imgs[0].img.height;
height = imgs[0].img.width;
break;
default:
width = imgs[0].img.width;
height = imgs[0].img.height;
}
} else {
width = imgs[0].img.width;
height = imgs[0].img.height;
}
calculatedWidth =
_config.size ? width / height > 1 ? _config.size : Math.floor((width / height) * _config.size) : width;
calculatedHeight =
_config.size ? width / height > 1 ? Math.floor((height / width) * _config.size) : _config.size : height;
_canvasSize.x = calculatedWidth;
_canvasSize.y = calculatedHeight;
loaded = true;
frameIdx = 0;
setTimeout(function() {
publishEvent("canrecord", []);
}, 0);
}, offset, size, _config.sequence);
}
function publishEvent(eventName, args) {
var j,
handlers = _eventHandlers[eventName];
if (handlers && handlers.length > 0) {
for ( j = 0; j < handlers.length; j++) {
handlers[j].apply(that, args);
}
}
}
that.trigger = publishEvent;
that.getWidth = function() {
return calculatedWidth;
};
that.getHeight = function() {
return calculatedHeight;
};
that.setWidth = function(newWidth) {
calculatedWidth = newWidth;
};
that.setHeight = function(newHeight) {
calculatedHeight = newHeight;
};
that.getRealWidth = function() {
return width;
};
that.getRealHeight = function() {
return height;
};
that.setInputStream = function(stream) {
_config = stream;
if (stream.sequence === false) {
baseUrl = stream.src;
size = 1;
} else {
baseUrl = stream.src;
size = stream.length;
}
loadImages();
};
that.ended = function() {
return ended;
};
that.setAttribute = function() {};
that.getConfig = function() {
return _config;
};
that.pause = function() {
paused = true;
};
that.play = function() {
paused = false;
};
that.setCurrentTime = function(time) {
frameIdx = time;
};
that.addEventListener = function(event, f) {
if (_eventNames.indexOf(event) !== -1) {
if (!_eventHandlers[event]) {
_eventHandlers[event] = [];
}
_eventHandlers[event].push(f);
}
};
that.setTopRight = function(topRight) {
_topRight.x = topRight.x;
_topRight.y = topRight.y;
};
that.getTopRight = function() {
return _topRight;
};
that.setCanvasSize = function(canvasSize) {
_canvasSize.x = canvasSize.x;
_canvasSize.y = canvasSize.y;
};
that.getCanvasSize = function() {
return _canvasSize;
};
that.getFrame = function() {
var frame;
if (!loaded){
return null;
}
if (!paused) {
frame = imgArray[frameIdx];
if (frameIdx < (size - 1)) {
frameIdx++;
} else {
setTimeout(function() {
ended = true;
publishEvent("ended", []);
}, 0);
}
}
return frame;
};
return that;
};
export default InputStream;

@ -0,0 +1,12 @@
import { VideoStream } from './video-stream';
export class LiveStream extends VideoStream {
constructor(video: HTMLVideoElement) {
video.setAttribute('autoplay', '');
super(video);
}
get ended(): boolean {
return false;
}
}

@ -0,0 +1,104 @@
import { ImageInfo } from './image-loader';
import { InputStream } from './input-stream';
import { InputStreamConfig } from './input-stream-config';
export class VideoStream extends InputStream {
private _video: HTMLVideoElement;
constructor(video: HTMLVideoElement) {
super();
this._video = video;
}
get realHeight(): number {
return this._video.videoHeight;
}
get realWidth(): number {
return this._video.videoWidth;
}
get config(): InputStreamConfig {
return this._config;
}
set config(config: InputStreamConfig) {
this._config = { ...config };
this._video.src = config.src || '';
}
get ended(): boolean {
return this._video.ended;
}
setAttribute(name: string, value: string): void {
this._video.setAttribute(name, value);
}
pause(): void {
this._video.pause();
}
play(): void {
this._video.play();
}
set currentTime(time: number) {
if (this._config.type !== 'LiveStream') {
this._video.currentTime = time;
}
}
addEventListener(event: string, listener: EventListener, options?: boolean): void {
super.addEventListener(event, listener, options);
if (this._eventNames.indexOf(event) === -1) {
this._video.addEventListener(event, listener, options);
}
}
clearEventHandlers(): void {
// TODO: come up with a way to remove video event handlers
// this._eventNames.forEach(eventName => {
// const handlers = this._eventHandlers.get(eventName);
// if (handlers && handlers.length > 0) {
// handlers.forEach(handler => this._video.removeEventListener(eventName, handler));
// }
// });
super.clearEventHandlers();
}
trigger(eventName: string, argArray?: any) {
if (eventName === 'canrecord') {
this._initSize();
}
super.trigger(eventName, argArray)
}
getFrame(): HTMLVideoElement | ImageInfo {
return this._video;
}
private _initSize() {
const width = this._video.videoWidth;
const height = this._video.videoHeight;
this._canvasWidth = this._calculatedWidth =
this._config.size ? width > height ? this._config.size : width * this._config.size / height | 0 : width;
this._canvasHeight = this._calculatedHeight =
this._config.size ? width > height ? height * this._config.size / width | 0 : this._config.size : height;
}
}
export class LiveStream extends VideoStream {
constructor(video: HTMLVideoElement) {
video.setAttribute('autoplay', '');
super(video);
}
get ended(): boolean {
return false;
}
}

@ -0,0 +1,71 @@
export type PatchSizeConfig = number | 'x-small' | 'small' | 'medium' | 'large' | 'x-large';
export interface BarcodeLocatorDebugConfig {
/**
* @default false
*/
showCanvas?: boolean;
/**
* @default false
*/
showPatches?: boolean;
/**
* @default false
*/
showFoundPatches?: boolean;
/**
* @default false
*/
showSkeleton?: boolean;
/**
* @default false
*/
showLabels?: boolean;
/**
* @default false
*/
showPatchLabels?: boolean;
/**
* @default false
*/
showRemainingPatchLabels?: boolean;
boxFromPatches?: {
/**
* @default false
*/
showTransformed?: boolean;
/**
* @default false
*/
showTransformedBox?: boolean;
/**
* @default false
*/
showBB?: boolean;
};
}
export interface BarcodeLocatorConfig {
debug?: BarcodeLocatorDebugConfig;
/**
* @default true
*/
halfSample?: boolean;
/**
* @default 'medium'
*/
patchSize?: PatchSizeConfig;
useWorker?: boolean;
}

@ -0,0 +1,135 @@
import { ImageWrapper } from '../common/image-wrapper';
import { Point } from '../common/point';
/**
* Invert matrix
* @param matrix the matrix to invert
* @returns the inverted matrix
*/
export function invert(matrix: Float32Array): Float32Array {
const a0 = matrix[0];
const a1 = matrix[1];
const a2 = matrix[2];
const a3 = matrix[3];
const determinant = a0 * a3 - a2 * a1;
if (!determinant) {
return null;
}
return new Float32Array([a3 / determinant, -a1 / determinant, -a2 / determinant, a0 / determinant]);
}
/**
* Transforms the vector with a matrix
* @param { x, y } vector to transform
* @param matrix matrix to transform with
* @returns the transformed vector
*/
export function transformWithMatrix({ x, y }: Point, matrix: Float32Array): Point {
return {
x: matrix[0] * x + matrix[2] * y,
y: matrix[1] * x + matrix[3] * y
};
}
function _computeHistogram(imageWrapper: ImageWrapper, bitsPerPixel: number): Int32Array {
if (!bitsPerPixel) {
bitsPerPixel = 8;
}
const imageData = imageWrapper.data;
const bitShift = 8 - bitsPerPixel;
const bucketCount = 1 << bitsPerPixel;
const histogram = new Int32Array(bucketCount);
for (let i = imageData.length; i--;) {
histogram[imageData[i] >> bitShift]++;
}
return histogram;
}
function _determineOtsuThreshold(imageWrapper: ImageWrapper, bitsPerPixel?: number): number {
if (!bitsPerPixel) {
bitsPerPixel = 8;
}
const bitShift = 8 - bitsPerPixel;
const hist = _computeHistogram(imageWrapper, bitsPerPixel);
const vet = [0];
const max = (1 << bitsPerPixel) - 1;
function px(init: number, end: number): number {
let sum = 0;
for (let i = init; i <= end; i++) {
sum += hist[i];
}
return sum;
}
function mx(init: number, end: number): number {
let sum = 0;
for (let i = init; i <= end; i++) {
sum += i * hist[i];
}
return sum;
}
for (let k = 1; k < max; k++) {
const p1 = px(0, k);
const p2 = px(k + 1, max);
const p12 = p1 * p2 || 1;
const m1 = mx(0, k) * p2;
const m2 = mx(k + 1, max) * p1;
const m12 = m1 - m2;
vet[k] = m12 * m12 / p12;
}
// index of max element
const threshold = vet.reduce((maxIndex, item, index, array) => item > array[maxIndex] ? index : maxIndex, 0);
return threshold << bitShift;
}
export function otsuThreshold(imageWrapper: ImageWrapper, targetWrapper: ImageWrapper): number {
const threshold = _determineOtsuThreshold(imageWrapper);
const targetData = targetWrapper.data;
imageWrapper.data.forEach((value, index) => {
targetData[index] = value < threshold ? 1 : 0;
});
return threshold;
}
/**
* @param imageWrapper input image to be sampled
* @param outImageWrapper {ImageWrapper} to be stored in
*/
export function halfSample(imageWrapper: ImageWrapper, outImageWrapper: ImageWrapper): void {
const image = imageWrapper.data;
const width = imageWrapper.size.x;
const outImage = outImageWrapper.data;
const endIndex = image.length;
const outWidth = width >> 1;
let topRowIndex = 0;
let bottomRowIndex = width;
let outImgIndex = 0;
while (bottomRowIndex < endIndex) {
for (let i = 0; i < outWidth; i++) {
outImage[outImgIndex] =
(image[topRowIndex] + image[topRowIndex + 1] + image[bottomRowIndex] + image[bottomRowIndex + 1]) >> 2;
outImgIndex++;
topRowIndex += 2;
bottomRowIndex += 2;
}
topRowIndex += width;
bottomRowIndex += width;
}
}

@ -0,0 +1,494 @@
import { Box } from '../common/box';
import { Cluster } from '../common/cluster';
import { HSV, hsv2rgb, RGB } from '../common/hsv2rgb';
import { ImageDebug } from '../common/image-debug';
import { ImageWrapper } from '../common/image-wrapper';
import { Moment } from '../common/moment';
import { Point } from '../common/point';
import { calculatePatchSize } from '../input/input-stream-utils';
import { BarcodeLocatorConfig } from './barcode-locator-config';
import { halfSample, invert, otsuThreshold, transformWithMatrix } from './barcode-locator-utils';
import { Rasterizer } from './rasterizer';
import skeletonizer from './skeletonizer';
import { SearchDirections } from './tracer';
interface Patch {
box: Box;
index: number;
moments: Array<Moment>;
pos: Point;
rad: number;
x: number;
y: number;
}
type Sceletonizer = any;
const MomentSimilarityThreshold = 0.9;
export class BarcodeLocator {
private _config: BarcodeLocatorConfig;
private _inputImageWrapper: ImageWrapper;
private _currentImageWrapper: ImageWrapper;
private _skelImageWrapper: ImageWrapper;
private _subImageWrapper: ImageWrapper;
private _labelImageWrapper: ImageWrapper<Array<number>>;
private _binaryImageWrapper: ImageWrapper;
private _patchGrid: ImageWrapper;
private _patchLabelGrid: ImageWrapper<Int32Array>;
private _imageToPatchGrid: Array<Patch>;
private _patchSize: Point;
private _binaryContext: CanvasRenderingContext2D;
private _numPatches: Point;
private _skeletonizer: Sceletonizer;
constructor(inputImageWrapper: ImageWrapper, config: BarcodeLocatorConfig) {
this._config = config;
this._inputImageWrapper = inputImageWrapper;
this._numPatches = { x: 0, y: 0 };
this._initBuffers();
this._initCanvas();
}
locate() {
if (this._config.halfSample) {
halfSample(this._inputImageWrapper, this._currentImageWrapper);
}
this._binarizeImage();
const patchesFound = this._findPatches();
// return unless 5% or more patches are found
if (patchesFound.length < this._numPatches.x * this._numPatches.y * 0.05) {
return null;
}
// rasterize area by comparing angular similarity;
const maxLabel = this._rasterizeAngularSimilarity(patchesFound);
if (maxLabel < 1) {
return null;
}
// search for area with the most patches (biggest connected area)
const topLabels = this._findBiggestConnectedAreas(maxLabel);
if (topLabels.length === 0) {
return null;
}
const boxes = this._findBoxes(topLabels, maxLabel);
return boxes;
}
private _initBuffers(): void {
if (this._config.halfSample) {
this._currentImageWrapper = new ImageWrapper({
x: this._inputImageWrapper.size.x / 2 | 0,
y: this._inputImageWrapper.size.y / 2 | 0
});
} else {
this._currentImageWrapper = this._inputImageWrapper;
}
this._patchSize = calculatePatchSize(this._config.patchSize, this._currentImageWrapper.size);
this._numPatches.x = this._currentImageWrapper.size.x / this._patchSize.x | 0;
this._numPatches.y = this._currentImageWrapper.size.y / this._patchSize.y | 0;
this._binaryImageWrapper = new ImageWrapper(this._currentImageWrapper.size, undefined, Uint8Array, false);
this._labelImageWrapper = new ImageWrapper(this._patchSize, undefined, Array, true);
const skeletonImageData = new ArrayBuffer(64 * 1024);
this._subImageWrapper = new ImageWrapper(this._patchSize, new Uint8Array(skeletonImageData, 0, this._patchSize.x * this._patchSize.y));
this._skelImageWrapper = new ImageWrapper(this._patchSize,
new Uint8Array(skeletonImageData, this._patchSize.x * this._patchSize.y * 3, this._patchSize.x * this._patchSize.y),
undefined, true);
this._skeletonizer = skeletonizer(
(typeof window !== 'undefined') ? window : (typeof self !== 'undefined') ? self : global,
{ size: this._patchSize.x },
skeletonImageData
);
const size = {
x: (this._currentImageWrapper.size.x / this._subImageWrapper.size.x) | 0,
y: (this._currentImageWrapper.size.y / this._subImageWrapper.size.y) | 0
};
this._patchLabelGrid = new ImageWrapper(size, undefined, Int32Array, true);
this._patchGrid = new ImageWrapper(size, undefined, undefined, true);
this._imageToPatchGrid = new Array<Patch>(this._patchLabelGrid.data.length);
}
private _initCanvas() {
if (this._config.useWorker || typeof document === 'undefined') {
return;
}
const canvas = document.createElement('canvas');
canvas.className = 'binaryBuffer';
canvas.width = this._binaryImageWrapper.size.x;
canvas.height = this._binaryImageWrapper.size.y;
if (process.env.NODE_ENV !== 'production' && this._config.debug && this._config.debug.showCanvas) {
document.querySelector('#debug').appendChild(canvas);
}
this._binaryContext = canvas.getContext('2d');
}
/**
* Creates a bounding box which encloses all the given patches
* @returns The minimal bounding box
*/
private _boxFromPatches(patches: Array<Patch>): Box {
const debug = process.env.NODE_ENV !== 'production' && this._config.debug;
let averageRad = patches.reduce((sum, { pos, rad }) => {
if (debug && debug.showPatches) {
// draw all patches which are to be taken into consideration
this._drawRect(pos, this._subImageWrapper.size, 'red', 1);
}
return sum + rad;
}, 0) / patches.length;
averageRad = (averageRad * 180 / Math.PI + 90) % 180 - 90;
if (averageRad < 0) {
averageRad += 180;
}
averageRad = (180 - averageRad) * Math.PI / 180;
const cos = Math.cos(averageRad);
const sin = Math.sin(averageRad);
const matrix = new Float32Array([cos, sin, -sin, cos]);
const inverseMatrix = invert(matrix);
// iterate over patches and rotate by angle
patches.forEach(({ box }) => {
for (let j = 0; j < 4; j++) {
box[j] = transformWithMatrix(box[j], matrix);
}
if (debug && debug.boxFromPatches.showTransformed) {
this._drawPath(box, '#99ff00', 2);
}
});
let minX = this._binaryImageWrapper.size.x;
let minY = this._binaryImageWrapper.size.y;
let maxX = -minX;
let maxY = -minY;
// find bounding box
patches.forEach(({ box }) => {
box.forEach(({ x, y }) => {
if (x < minX) {
minX = x;
}
if (x > maxX) {
maxX = x;
}
if (y < minY) {
minY = y;
}
if (y > maxY) {
maxY = y;
}
});
});
let box: Box = [{ x: minX, y: minY }, { x: maxX, y: minY }, { x: maxX, y: maxY }, { x: minX, y: maxY }];
if (debug && debug.boxFromPatches.showTransformedBox) {
this._drawPath(box, '#ff0000', 2);
}
// reverse rotation
box = box.map(vertex => transformWithMatrix(vertex, inverseMatrix)) as Box;
if (debug && debug.boxFromPatches.showBB) {
this._drawPath(box, '#ff0000', 2);
}
if (this._config.halfSample) {
// scale
box = box.map(({ x, y }) => ({ x: x * 2, y: y *= 2 })) as Box;
}
return box;
}
/**
* Creates a binary image of the current image
*/
private _binarizeImage(): void {
otsuThreshold(this._currentImageWrapper, this._binaryImageWrapper);
this._binaryImageWrapper.zeroBorder();
if (process.env.NODE_ENV !== 'production' && this._config.debug && this._config.debug.showCanvas) {
this._binaryImageWrapper.show(this._binaryContext, 255);
}
}
/**
* Iterate over the entire image, extract patches
*/
private _findPatches(): Array<Patch> {
const debug = process.env.NODE_ENV !== 'production' && this._config.debug;
let patchesFound = new Array<Patch>();
for (let i = 0; i < this._numPatches.x; i++) {
for (let j = 0; j < this._numPatches.y; j++) {
const x = this._subImageWrapper.size.x * i;
const y = this._subImageWrapper.size.y * j;
// seperate parts
this._skeletonize(x, y);
// Rasterize, find individual bars
this._skelImageWrapper.zeroBorder();
this._labelImageWrapper.data.fill(0);
const rasterizer = new Rasterizer(this._skelImageWrapper, this._labelImageWrapper);
const rasterResult = rasterizer.rasterize(0);
if (debug && debug.showLabels) {
this._labelImageWrapper.overlay(this._binaryContext, 360 / rasterResult.count | 0, x, y);
}
// calculate moments from the skeletonized patch
const moments = this._labelImageWrapper.moments(rasterResult.count);
// extract eligible patches
const patch = this._describePatch(moments, j * this._numPatches.x + i, x, y);
if (patch) {
patchesFound.push(patch);
if (debug && debug.showFoundPatches) {
this._drawRect(patch.pos, this._subImageWrapper.size, '#99ff00', 2);
}
}
}
}
return patchesFound;
}
/**
* Finds those connected areas which contain at least 6 patches
* and returns them ordered DESC by the number of contained patches
* @param maxLabel
*/
private _findBiggestConnectedAreas(maxLabel: number): Array<number> {
let labelHist = new Array<number>(maxLabel).fill(0);
this._patchLabelGrid.data.forEach((data: number) => {
if (data > 0) {
labelHist[data - 1]++;
}
});
// extract top areas with at least 6 patches present
const topLabels = labelHist.map((value, index) => ({ value, index }))
.filter(({ value }) => value >= 5).sort((a, b) => b.value - a.value).map(({ index }) => index + 1);
return topLabels;
}
/**
*
*/
private _findBoxes(topLabels: Array<number>, maxLabel: number): Array<Box> {
const boxes = new Array<Box>();
const showRemainingPatchLabels = process.env.NODE_ENV !== 'production' &&
this._config.debug && this._config.debug.showRemainingPatchLabels;
topLabels.forEach(label => {
const patches = new Array<Patch>();
this._patchLabelGrid.data.forEach((data: number, index: number) => {
if (data === label) {
patches.push(this._imageToPatchGrid[index]);
}
});
const box = this._boxFromPatches(patches);
if (box) {
boxes.push(box);
if (showRemainingPatchLabels) {
// draw patch-labels if requested
const hsv: HSV = [(label / (maxLabel + 1)) * 360, 1, 1];
const rgb: RGB = [0, 0, 0];
hsv2rgb(hsv, rgb);
const color = `rgb(${rgb.join(',')})`;
patches.forEach(({ pos }) => this._drawRect(pos, this._subImageWrapper.size, color, 2));
}
}
});
return boxes;
}
/**
* Find similar moments (via cluster)
* @param moments
*/
private _similarMoments(moments: Array<Moment>): Array<Moment> {
const clusters = Cluster.clusterize(moments, MomentSimilarityThreshold);
const topCluster = clusters.reduce((top, item) => {
const count = item.moments.length;
return count > top.count ? { item, count } : top;
}, { item: { moments: [] }, count: 0 });
const result = topCluster.item.moments;
return result;
}
private _skeletonize(x: number, y: number): void {
this._binaryImageWrapper.subImageAsCopy(this._subImageWrapper, x, y);
this._skeletonizer.skeletonize();
// Show skeleton if requested
if (process.env.NODE_ENV !== 'production' && this._config.debug && this._config.debug.showSkeleton) {
this._skelImageWrapper.overlay(this._binaryContext, 360, x, y);
}
}
/**
* Extracts and describes those patches which seem to contain a barcode pattern
* @param moments
* @param index
* @param x
* @param y
* @returns list of patches
*/
private _describePatch(moments: Array<Moment>, index: number, x: number, y: number): Patch {
if (moments.length > 1) {
const minComponentWeight = Math.ceil(this._patchSize.x / 3);
// only collect moments which area covers at least minComponentWeight pixels
const eligibleMoments = moments.filter(moment => moment.m00 > minComponentWeight);
// if at least 2 moments are found which have at least minComponentWeights covered
if (eligibleMoments.length > 1) {
const matchingMoments = this._similarMoments(eligibleMoments);
const length = matchingMoments.length | 0;
// Only two of the moments are allowed not to fit into the equation
if (length > 1 && (length << 2) >= eligibleMoments.length * 3 && (length << 2) > moments.length) {
// determine the similarity of the moments
const rad = matchingMoments.reduce((sum: number, moment: Moment) => sum + moment.rad, 0) / length;
return {
index,
pos: { x, y },
box: [
{ x, y },
{ x: x + this._subImageWrapper.size.x, y },
{ x: x + this._subImageWrapper.size.x, y: y + this._subImageWrapper.size.y },
{ x, y: y + this._subImageWrapper.size.y }
],
moments: matchingMoments,
rad,
x: Math.cos(rad),
y: Math.sin(rad)
};
}
}
}
return null;
}
private _notYetProcessed(): number {
for (let i = 0; i < this._patchLabelGrid.data.length; i++) {
if (this._patchLabelGrid.data[i] === 0 && this._patchGrid.data[i] === 1) {
return i;
}
}
return this._patchLabelGrid.data.length;
}
private _trace(currentIndex: number, label: number): void {
const threshold = 0.95;
const current: Point = {
x: currentIndex % this._patchLabelGrid.size.x,
y: (currentIndex / this._patchLabelGrid.size.x) | 0
};
if (currentIndex < this._patchLabelGrid.data.length) {
const currentPatch = this._imageToPatchGrid[currentIndex];
// assign label
this._patchLabelGrid.data[currentIndex] = label;
SearchDirections.forEach(direction => {
const y = current.y + direction[0];
const x = current.x + direction[1];
const index = y * this._patchLabelGrid.size.x + x;
// continue if patch empty
if (this._patchGrid.data[index] === 0) {
this._patchLabelGrid.data[index] = Number.MAX_VALUE;
} else if (this._patchLabelGrid.data[index] === 0) {
const patch = this._imageToPatchGrid[index];
const similarity = Math.abs(patch.x * currentPatch.x + patch.y * currentPatch.y);
if (similarity > threshold) {
this._trace(index, label);
}
}
});
}
}
/**
* Finds patches which are connected and share the same orientation
* @param patchesFound
*/
private _rasterizeAngularSimilarity(patchesFound: Array<Patch>): number {
let label = 0;
const hsv: HSV = [0, 1, 1];
const rgb: RGB = [0, 0, 0];
// prepare for finding the right patches
this._patchGrid.data.fill(0);
this._patchLabelGrid.data.fill(0);
this._imageToPatchGrid.fill(null);
patchesFound.forEach(patch => {
this._imageToPatchGrid[patch.index] = patch;
this._patchGrid.data[patch.index] = 1;
});
// rasterize the patches found to determine area
this._patchGrid.zeroBorder();
let currentIndex = 0;
while ((currentIndex = this._notYetProcessed()) < this._patchLabelGrid.data.length) {
label++;
this._trace(currentIndex, label);
}
// draw patch-labels if requested
if (process.env.NODE_ENV !== 'production' && this._config.debug && this._config.debug.showPatchLabels) {
for (let j = 0; j < this._patchLabelGrid.data.length; j++) {
if (this._patchLabelGrid.data[j] > 0 && this._patchLabelGrid.data[j] <= label) {
const patch = this._imageToPatchGrid[j];
hsv[0] = (this._patchLabelGrid.data[j] / (label + 1)) * 360;
hsv2rgb(hsv, rgb);
this._drawRect(patch.pos, this._subImageWrapper.size, `rgb(${rgb.join(',')})`, 2);
}
}
}
return label;
}
private _drawRect({ x, y }: Point, size: Point, color: string, lineWidth: number): void {
this._binaryContext.strokeStyle = color;
this._binaryContext.fillStyle = color;
this._binaryContext.lineWidth = lineWidth || 1;
this._binaryContext.strokeRect(x, y, size.x, size.y);
}
private _drawPath(path: Array<Point>, color: string, lineWidth: number): void {
ImageDebug.drawPath(path, this._binaryContext, color, lineWidth);
}
}

@ -1,607 +0,0 @@
import ImageWrapper from '../common/image_wrapper';
import {
calculatePatchSize,
otsuThreshold,
hsv2rgb,
cluster,
topGeneric,
imageRef,
halfSample,
computeImageArea
} from '../common/cv_utils';
import ArrayHelper from '../common/array_helper';
import ImageDebug from '../common/image_debug';
import Rasterizer from './rasterizer';
import Tracer from './tracer';
import skeletonizer from './skeletonizer';
const vec2 = {
clone: require('gl-vec2/clone'),
dot: require('gl-vec2/dot'),
scale: require('gl-vec2/scale'),
transformMat2: require('gl-vec2/transformMat2')
};
const mat2 = {
copy: require('gl-mat2/copy'),
create: require('gl-mat2/create'),
invert: require('gl-mat2/invert')
}
var _config,
_currentImageWrapper,
_skelImageWrapper,
_subImageWrapper,
_labelImageWrapper,
_patchGrid,
_patchLabelGrid,
_imageToPatchGrid,
_binaryImageWrapper,
_patchSize,
_canvasContainer = {
ctx: {
binary: null
},
dom: {
binary: null
}
},
_numPatches = {x: 0, y: 0},
_inputImageWrapper,
_skeletonizer;
function initBuffers() {
var skeletonImageData;
if (_config.halfSample) {
_currentImageWrapper = new ImageWrapper({
x: _inputImageWrapper.size.x / 2 | 0,
y: _inputImageWrapper.size.y / 2 | 0
});
} else {
_currentImageWrapper = _inputImageWrapper;
}
_patchSize = calculatePatchSize(_config.patchSize, _currentImageWrapper.size);
_numPatches.x = _currentImageWrapper.size.x / _patchSize.x | 0;
_numPatches.y = _currentImageWrapper.size.y / _patchSize.y | 0;
_binaryImageWrapper = new ImageWrapper(_currentImageWrapper.size, undefined, Uint8Array, false);
_labelImageWrapper = new ImageWrapper(_patchSize, undefined, Array, true);
skeletonImageData = new ArrayBuffer(64 * 1024);
_subImageWrapper = new ImageWrapper(_patchSize,
new Uint8Array(skeletonImageData, 0, _patchSize.x * _patchSize.y));
_skelImageWrapper = new ImageWrapper(_patchSize,
new Uint8Array(skeletonImageData, _patchSize.x * _patchSize.y * 3, _patchSize.x * _patchSize.y),
undefined, true);
_skeletonizer = skeletonizer((typeof window !== 'undefined') ? window : (typeof self !== 'undefined') ? self : global, {
size: _patchSize.x
}, skeletonImageData);
_imageToPatchGrid = new ImageWrapper({
x: (_currentImageWrapper.size.x / _subImageWrapper.size.x) | 0,
y: (_currentImageWrapper.size.y / _subImageWrapper.size.y) | 0
}, undefined, Array, true);
_patchGrid = new ImageWrapper(_imageToPatchGrid.size, undefined, undefined, true);
_patchLabelGrid = new ImageWrapper(_imageToPatchGrid.size, undefined, Int32Array, true);
}
function initCanvas() {
if (_config.useWorker || typeof document === 'undefined') {
return;
}
_canvasContainer.dom.binary = document.createElement("canvas");
_canvasContainer.dom.binary.className = "binaryBuffer";
if (ENV.development && _config.debug.showCanvas === true) {
document.querySelector("#debug").appendChild(_canvasContainer.dom.binary);
}
_canvasContainer.ctx.binary = _canvasContainer.dom.binary.getContext("2d");
_canvasContainer.dom.binary.width = _binaryImageWrapper.size.x;
_canvasContainer.dom.binary.height = _binaryImageWrapper.size.y;
}
/**
* Creates a bounding box which encloses all the given patches
* @returns {Array} The minimal bounding box
*/
function boxFromPatches(patches) {
var overAvg,
i,
j,
patch,
transMat,
minx =
_binaryImageWrapper.size.x,
miny = _binaryImageWrapper.size.y,
maxx = -_binaryImageWrapper.size.x,
maxy = -_binaryImageWrapper.size.y,
box,
scale;
// draw all patches which are to be taken into consideration
overAvg = 0;
for ( i = 0; i < patches.length; i++) {
patch = patches[i];
overAvg += patch.rad;
if (ENV.development && _config.debug.showPatches) {
ImageDebug.drawRect(patch.pos, _subImageWrapper.size, _canvasContainer.ctx.binary, {color: "red"});
}
}
overAvg /= patches.length;
overAvg = (overAvg * 180 / Math.PI + 90) % 180 - 90;
if (overAvg < 0) {
overAvg += 180;
}
overAvg = (180 - overAvg) * Math.PI / 180;
transMat = mat2.copy(mat2.create(), [Math.cos(overAvg), Math.sin(overAvg), -Math.sin(overAvg), Math.cos(overAvg)]);
// iterate over patches and rotate by angle
for ( i = 0; i < patches.length; i++) {
patch = patches[i];
for ( j = 0; j < 4; j++) {
vec2.transformMat2(patch.box[j], patch.box[j], transMat);
}
if (ENV.development && _config.debug.boxFromPatches.showTransformed) {
ImageDebug.drawPath(patch.box, {x: 0, y: 1}, _canvasContainer.ctx.binary, {color: '#99ff00', lineWidth: 2});
}
}
// find bounding box
for ( i = 0; i < patches.length; i++) {
patch = patches[i];
for ( j = 0; j < 4; j++) {
if (patch.box[j][0] < minx) {
minx = patch.box[j][0];
}
if (patch.box[j][0] > maxx) {
maxx = patch.box[j][0];
}
if (patch.box[j][1] < miny) {
miny = patch.box[j][1];
}
if (patch.box[j][1] > maxy) {
maxy = patch.box[j][1];
}
}
}
box = [[minx, miny], [maxx, miny], [maxx, maxy], [minx, maxy]];
if (ENV.development && _config.debug.boxFromPatches.showTransformedBox) {
ImageDebug.drawPath(box, {x: 0, y: 1}, _canvasContainer.ctx.binary, {color: '#ff0000', lineWidth: 2});
}
scale = _config.halfSample ? 2 : 1;
// reverse rotation;
transMat = mat2.invert(transMat, transMat);
for ( j = 0; j < 4; j++) {
vec2.transformMat2(box[j], box[j], transMat);
}
if (ENV.development && _config.debug.boxFromPatches.showBB) {
ImageDebug.drawPath(box, {x: 0, y: 1}, _canvasContainer.ctx.binary, {color: '#ff0000', lineWidth: 2});
}
for ( j = 0; j < 4; j++) {
vec2.scale(box[j], box[j], scale);
}
return box;
}
/**
* Creates a binary image of the current image
*/
function binarizeImage() {
otsuThreshold(_currentImageWrapper, _binaryImageWrapper);
_binaryImageWrapper.zeroBorder();
if (ENV.development && _config.debug.showCanvas) {
_binaryImageWrapper.show(_canvasContainer.dom.binary, 255);
}
}
/**
* Iterate over the entire image
* extract patches
*/
function findPatches() {
var i,
j,
x,
y,
moments,
patchesFound = [],
rasterizer,
rasterResult,
patch;
for (i = 0; i < _numPatches.x; i++) {
for (j = 0; j < _numPatches.y; j++) {
x = _subImageWrapper.size.x * i;
y = _subImageWrapper.size.y * j;
// seperate parts
skeletonize(x, y);
// Rasterize, find individual bars
_skelImageWrapper.zeroBorder();
ArrayHelper.init(_labelImageWrapper.data, 0);
rasterizer = Rasterizer.create(_skelImageWrapper, _labelImageWrapper);
rasterResult = rasterizer.rasterize(0);
if (ENV.development && _config.debug.showLabels) {
_labelImageWrapper.overlay(_canvasContainer.dom.binary, Math.floor(360 / rasterResult.count),
{x: x, y: y});
}
// calculate moments from the skeletonized patch
moments = _labelImageWrapper.moments(rasterResult.count);
// extract eligible patches
patchesFound = patchesFound.concat(describePatch(moments, [i, j], x, y));
}
}
if (ENV.development && _config.debug.showFoundPatches) {
for ( i = 0; i < patchesFound.length; i++) {
patch = patchesFound[i];
ImageDebug.drawRect(patch.pos, _subImageWrapper.size, _canvasContainer.ctx.binary,
{color: "#99ff00", lineWidth: 2});
}
}
return patchesFound;
}
/**
* Finds those connected areas which contain at least 6 patches
* and returns them ordered DESC by the number of contained patches
* @param {Number} maxLabel
*/
function findBiggestConnectedAreas(maxLabel){
var i,
sum,
labelHist = [],
topLabels = [];
for ( i = 0; i < maxLabel; i++) {
labelHist.push(0);
}
sum = _patchLabelGrid.data.length;
while (sum--) {
if (_patchLabelGrid.data[sum] > 0) {
labelHist[_patchLabelGrid.data[sum] - 1]++;
}
}
labelHist = labelHist.map(function(val, idx) {
return {
val: val,
label: idx + 1
};
});
labelHist.sort(function(a, b) {
return b.val - a.val;
});
// extract top areas with at least 6 patches present
topLabels = labelHist.filter(function(el) {
return el.val >= 5;
});
return topLabels;
}
/**
*
*/
function findBoxes(topLabels, maxLabel) {
var i,
j,
sum,
patches = [],
patch,
box,
boxes = [],
hsv = [0, 1, 1],
rgb = [0, 0, 0];
for ( i = 0; i < topLabels.length; i++) {
sum = _patchLabelGrid.data.length;
patches.length = 0;
while (sum--) {
if (_patchLabelGrid.data[sum] === topLabels[i].label) {
patch = _imageToPatchGrid.data[sum];
patches.push(patch);
}
}
box = boxFromPatches(patches);
if (box) {
boxes.push(box);
// draw patch-labels if requested
if (ENV.development && _config.debug.showRemainingPatchLabels) {
for ( j = 0; j < patches.length; j++) {
patch = patches[j];
hsv[0] = (topLabels[i].label / (maxLabel + 1)) * 360;
hsv2rgb(hsv, rgb);
ImageDebug.drawRect(patch.pos, _subImageWrapper.size, _canvasContainer.ctx.binary,
{color: "rgb(" + rgb.join(",") + ")", lineWidth: 2});
}
}
}
}
return boxes;
}
/**
* Find similar moments (via cluster)
* @param {Object} moments
*/
function similarMoments(moments) {
var clusters = cluster(moments, 0.90);
var topCluster = topGeneric(clusters, 1, function(e) {
return e.getPoints().length;
});
var points = [], result = [];
if (topCluster.length === 1) {
points = topCluster[0].item.getPoints();
for (var i = 0; i < points.length; i++) {
result.push(points[i].point);
}
}
return result;
}
function skeletonize(x, y) {
_binaryImageWrapper.subImageAsCopy(_subImageWrapper, imageRef(x, y));
_skeletonizer.skeletonize();
// Show skeleton if requested
if (ENV.development && _config.debug.showSkeleton) {
_skelImageWrapper.overlay(_canvasContainer.dom.binary, 360, imageRef(x, y));
}
}
/**
* Extracts and describes those patches which seem to contain a barcode pattern
* @param {Array} moments
* @param {Object} patchPos,
* @param {Number} x
* @param {Number} y
* @returns {Array} list of patches
*/
function describePatch(moments, patchPos, x, y) {
var k,
avg,
eligibleMoments = [],
matchingMoments,
patch,
patchesFound = [],
minComponentWeight = Math.ceil(_patchSize.x / 3);
if (moments.length >= 2) {
// only collect moments which's area covers at least minComponentWeight pixels.
for ( k = 0; k < moments.length; k++) {
if (moments[k].m00 > minComponentWeight) {
eligibleMoments.push(moments[k]);
}
}
// if at least 2 moments are found which have at least minComponentWeights covered
if (eligibleMoments.length >= 2) {
matchingMoments = similarMoments(eligibleMoments);
avg = 0;
// determine the similarity of the moments
for ( k = 0; k < matchingMoments.length; k++) {
avg += matchingMoments[k].rad;
}
// Only two of the moments are allowed not to fit into the equation
// add the patch to the set
if (matchingMoments.length > 1
&& matchingMoments.length >= (eligibleMoments.length / 4) * 3
&& matchingMoments.length > moments.length / 4) {
avg /= matchingMoments.length;
patch = {
index: patchPos[1] * _numPatches.x + patchPos[0],
pos: {
x: x,
y: y
},
box: [
vec2.clone([x, y]),
vec2.clone([x + _subImageWrapper.size.x, y]),
vec2.clone([x + _subImageWrapper.size.x, y + _subImageWrapper.size.y]),
vec2.clone([x, y + _subImageWrapper.size.y])
],
moments: matchingMoments,
rad: avg,
vec: vec2.clone([Math.cos(avg), Math.sin(avg)])
};
patchesFound.push(patch);
}
}
}
return patchesFound;
}
/**
* finds patches which are connected and share the same orientation
* @param {Object} patchesFound
*/
function rasterizeAngularSimilarity(patchesFound) {
var label = 0,
threshold = 0.95,
currIdx = 0,
j,
patch,
hsv = [0, 1, 1],
rgb = [0, 0, 0];
function notYetProcessed() {
var i;
for ( i = 0; i < _patchLabelGrid.data.length; i++) {
if (_patchLabelGrid.data[i] === 0 && _patchGrid.data[i] === 1) {
return i;
}
}
return _patchLabelGrid.length;
}
function trace(currentIdx) {
var x,
y,
currentPatch,
idx,
dir,
current = {
x: currentIdx % _patchLabelGrid.size.x,
y: (currentIdx / _patchLabelGrid.size.x) | 0
},
similarity;
if (currentIdx < _patchLabelGrid.data.length) {
currentPatch = _imageToPatchGrid.data[currentIdx];
// assign label
_patchLabelGrid.data[currentIdx] = label;
for ( dir = 0; dir < Tracer.searchDirections.length; dir++) {
y = current.y + Tracer.searchDirections[dir][0];
x = current.x + Tracer.searchDirections[dir][1];
idx = y * _patchLabelGrid.size.x + x;
// continue if patch empty
if (_patchGrid.data[idx] === 0) {
_patchLabelGrid.data[idx] = Number.MAX_VALUE;
continue;
}
if (_patchLabelGrid.data[idx] === 0) {
similarity = Math.abs(vec2.dot(_imageToPatchGrid.data[idx].vec, currentPatch.vec));
if (similarity > threshold) {
trace(idx);
}
}
}
}
}
// prepare for finding the right patches
ArrayHelper.init(_patchGrid.data, 0);
ArrayHelper.init(_patchLabelGrid.data, 0);
ArrayHelper.init(_imageToPatchGrid.data, null);
for ( j = 0; j < patchesFound.length; j++) {
patch = patchesFound[j];
_imageToPatchGrid.data[patch.index] = patch;
_patchGrid.data[patch.index] = 1;
}
// rasterize the patches found to determine area
_patchGrid.zeroBorder();
while (( currIdx = notYetProcessed()) < _patchLabelGrid.data.length) {
label++;
trace(currIdx);
}
// draw patch-labels if requested
if (ENV.development && _config.debug.showPatchLabels) {
for ( j = 0; j < _patchLabelGrid.data.length; j++) {
if (_patchLabelGrid.data[j] > 0 && _patchLabelGrid.data[j] <= label) {
patch = _imageToPatchGrid.data[j];
hsv[0] = (_patchLabelGrid.data[j] / (label + 1)) * 360;
hsv2rgb(hsv, rgb);
ImageDebug.drawRect(patch.pos, _subImageWrapper.size, _canvasContainer.ctx.binary,
{color: "rgb(" + rgb.join(",") + ")", lineWidth: 2});
}
}
}
return label;
}
export default {
init: function(inputImageWrapper, config) {
_config = config;
_inputImageWrapper = inputImageWrapper;
initBuffers();
initCanvas();
},
locate: function() {
var patchesFound,
topLabels,
boxes;
if (_config.halfSample) {
halfSample(_inputImageWrapper, _currentImageWrapper);
}
binarizeImage();
patchesFound = findPatches();
// return unless 5% or more patches are found
if (patchesFound.length < _numPatches.x * _numPatches.y * 0.05) {
return null;
}
// rasterrize area by comparing angular similarity;
var maxLabel = rasterizeAngularSimilarity(patchesFound);
if (maxLabel < 1) {
return null;
}
// search for area with the most patches (biggest connected area)
topLabels = findBiggestConnectedAreas(maxLabel);
if (topLabels.length === 0) {
return null;
}
boxes = findBoxes(topLabels, maxLabel);
return boxes;
},
checkImageConstraints: function(inputStream, config) {
var patchSize,
width = inputStream.getWidth(),
height = inputStream.getHeight(),
halfSample = config.halfSample ? 0.5 : 1,
size,
area;
// calculate width and height based on area
if (inputStream.getConfig().area) {
area = computeImageArea(width, height, inputStream.getConfig().area);
inputStream.setTopRight({x: area.sx, y: area.sy});
inputStream.setCanvasSize({x: width, y: height});
width = area.sw;
height = area.sh;
}
size = {
x: Math.floor(width * halfSample),
y: Math.floor(height * halfSample)
};
patchSize = calculatePatchSize(config.patchSize, size);
if (ENV.development) {
console.log("Patch-Size: " + JSON.stringify(patchSize));
}
inputStream.setWidth(Math.floor(Math.floor(size.x / patchSize.x) * (1 / halfSample) * patchSize.x));
inputStream.setHeight(Math.floor(Math.floor(size.y / patchSize.y) * (1 / halfSample) * patchSize.y));
if ((inputStream.getWidth() % patchSize.x) === 0 && (inputStream.getHeight() % patchSize.y) === 0) {
return true;
}
throw new Error("Image dimensions do not comply with the current settings: Width (" +
width + " )and height (" + height +
") must a multiple of " + patchSize.x);
}
};

@ -1,195 +0,0 @@
import Tracer from './tracer';
/**
* http://www.codeproject.com/Tips/407172/Connected-Component-Labeling-and-Vectorization
*/
var Rasterizer = {
createContour2D: function() {
return {
dir: null,
index: null,
firstVertex: null,
insideContours: null,
nextpeer: null,
prevpeer: null
};
},
CONTOUR_DIR: {
CW_DIR: 0,
CCW_DIR: 1,
UNKNOWN_DIR: 2
},
DIR: {
OUTSIDE_EDGE: -32767,
INSIDE_EDGE: -32766
},
create: function(imageWrapper, labelWrapper) {
var imageData = imageWrapper.data,
labelData = labelWrapper.data,
width = imageWrapper.size.x,
height = imageWrapper.size.y,
tracer = Tracer.create(imageWrapper, labelWrapper);
return {
rasterize: function(depthlabel) {
var color,
bc,
lc,
labelindex,
cx,
cy,
colorMap = [],
vertex,
p,
cc,
sc,
pos,
connectedCount = 0,
i;
for ( i = 0; i < 400; i++) {
colorMap[i] = 0;
}
colorMap[0] = imageData[0];
cc = null;
for ( cy = 1; cy < height - 1; cy++) {
labelindex = 0;
bc = colorMap[0];
for ( cx = 1; cx < width - 1; cx++) {
pos = cy * width + cx;
if (labelData[pos] === 0) {
color = imageData[pos];
if (color !== bc) {
if (labelindex === 0) {
lc = connectedCount + 1;
colorMap[lc] = color;
bc = color;
vertex = tracer.contourTracing(cy, cx, lc, color, Rasterizer.DIR.OUTSIDE_EDGE);
if (vertex !== null) {
connectedCount++;
labelindex = lc;
p = Rasterizer.createContour2D();
p.dir = Rasterizer.CONTOUR_DIR.CW_DIR;
p.index = labelindex;
p.firstVertex = vertex;
p.nextpeer = cc;
p.insideContours = null;
if (cc !== null) {
cc.prevpeer = p;
}
cc = p;
}
} else {
vertex = tracer
.contourTracing(cy, cx, Rasterizer.DIR.INSIDE_EDGE, color, labelindex);
if (vertex !== null) {
p = Rasterizer.createContour2D();
p.firstVertex = vertex;
p.insideContours = null;
if (depthlabel === 0) {
p.dir = Rasterizer.CONTOUR_DIR.CCW_DIR;
} else {
p.dir = Rasterizer.CONTOUR_DIR.CW_DIR;
}
p.index = depthlabel;
sc = cc;
while ((sc !== null) && sc.index !== labelindex) {
sc = sc.nextpeer;
}
if (sc !== null) {
p.nextpeer = sc.insideContours;
if (sc.insideContours !== null) {
sc.insideContours.prevpeer = p;
}
sc.insideContours = p;
}
}
}
} else {
labelData[pos] = labelindex;
}
} else if (labelData[pos] === Rasterizer.DIR.OUTSIDE_EDGE
|| labelData[pos] === Rasterizer.DIR.INSIDE_EDGE) {
labelindex = 0;
if (labelData[pos] === Rasterizer.DIR.INSIDE_EDGE) {
bc = imageData[pos];
} else {
bc = colorMap[0];
}
} else {
labelindex = labelData[pos];
bc = colorMap[labelindex];
}
}
}
sc = cc;
while (sc !== null) {
sc.index = depthlabel;
sc = sc.nextpeer;
}
return {
cc: cc,
count: connectedCount
};
},
debug: {
drawContour: function(canvas, firstContour) {
var ctx = canvas.getContext("2d"),
pq = firstContour,
iq,
q,
p;
ctx.strokeStyle = "red";
ctx.fillStyle = "red";
ctx.lineWidth = 1;
if (pq !== null) {
iq = pq.insideContours;
} else {
iq = null;
}
while (pq !== null) {
if (iq !== null) {
q = iq;
iq = iq.nextpeer;
} else {
q = pq;
pq = pq.nextpeer;
if (pq !== null) {
iq = pq.insideContours;
} else {
iq = null;
}
}
switch (q.dir) {
case Rasterizer.CONTOUR_DIR.CW_DIR:
ctx.strokeStyle = "red";
break;
case Rasterizer.CONTOUR_DIR.CCW_DIR:
ctx.strokeStyle = "blue";
break;
case Rasterizer.CONTOUR_DIR.UNKNOWN_DIR:
ctx.strokeStyle = "green";
break;
}
p = q.firstVertex;
ctx.beginPath();
ctx.moveTo(p.x, p.y);
do {
p = p.next;
ctx.lineTo(p.x, p.y);
} while (p !== q.firstVertex);
ctx.stroke();
}
}
}
};
}
};
export default Rasterizer;

@ -0,0 +1,188 @@
/**
* @borrows http://www.codeproject.com/Tips/407172/Connected-Component-Labeling-and-Vectorization
*/
import { ImageWrapper } from '../common/image-wrapper';
import { ContourVertex, Tracer } from './tracer';
enum EdgeLabel {
Outside = -32767,
Inside = -32766
};
enum ContourDirection {
CW = 0,
CCW = 1,
Unknown = 2
};
interface Contour {
dir: ContourDirection;
index: number;
firstVertex: ContourVertex;
previousPeer?: Contour;
nextPeer?: Contour;
insideContours: Contour;
}
export interface RasterResult {
cc: Contour;
count: number;
}
export class Rasterizer {
private _width: number;
private _height: number;
private _tracer: Tracer;
private _imageData: Uint8Array;
private _labelData: Array<number>;
constructor(imageWrapper: ImageWrapper<Uint8Array>, labelWrapper: ImageWrapper<Array<number>>) {
this._imageData = imageWrapper.data;
this._labelData = labelWrapper.data as Array<number>;
this._width = imageWrapper.size.x;
this._height = imageWrapper.size.y;
this._tracer = new Tracer(imageWrapper, labelWrapper);
}
rasterize(depthLabel: number): RasterResult {
const colorMap = new Array<number>();
for (let i = 0; i < 400; i++) {
colorMap[i] = 0;
}
colorMap[0] = this._imageData[0];
let cc: Contour = null;
let sc: Contour;
let connectedCount = 0;
for (let cy = 1; cy < this._height - 1; cy++) {
let labelIndex = 0;
let bc = colorMap[0];
for (let cx = 1; cx < this._width - 1; cx++) {
const pos = cy * this._width + cx;
if (this._labelData[pos] === 0) {
const color = this._imageData[pos];
if (color !== bc) {
if (labelIndex === 0) {
const lc = connectedCount + 1;
colorMap[lc] = color;
bc = color;
const vertex = this._tracer.contourTracing(cy, cx, lc, color, EdgeLabel.Outside);
if (vertex !== null) {
connectedCount++;
labelIndex = lc;
const p: Contour = {
dir: ContourDirection.CW,
index: labelIndex,
firstVertex: vertex,
nextPeer: cc,
insideContours: null
};
if (cc !== null) {
cc.previousPeer = p;
}
cc = p;
}
} else {
const vertex = this._tracer.contourTracing(cy, cx, EdgeLabel.Inside, color, labelIndex);
if (vertex !== null) {
const p: Contour = {
dir: depthLabel === 0 ? ContourDirection.CCW : ContourDirection.CW,
firstVertex: vertex,
index: depthLabel,
insideContours: null
};
sc = cc;
while ((sc !== null) && sc.index !== labelIndex) {
sc = sc.nextPeer;
}
if (sc !== null) {
p.nextPeer = sc.insideContours;
if (sc.insideContours !== null) {
sc.insideContours.previousPeer = p;
}
sc.insideContours = p;
}
}
}
} else {
this._labelData[pos] = labelIndex;
}
} else if (this._labelData[pos] === EdgeLabel.Inside) {
labelIndex = 0;
bc = this._imageData[pos];
} else if (this._labelData[pos] === EdgeLabel.Outside) {
labelIndex = 0;
bc = colorMap[0];
} else {
labelIndex = this._labelData[pos];
bc = colorMap[labelIndex];
}
}
}
sc = cc;
while (sc !== null) {
sc.index = depthLabel;
sc = sc.nextPeer;
}
return {
cc,
count: connectedCount
};
}
drawContour(canvas: HTMLCanvasElement, firstContour: Contour): void {
const context = canvas.getContext('2d');
context.strokeStyle = 'red';
context.fillStyle = 'red';
context.lineWidth = 1;
let pq = firstContour;
let iq = pq && pq.insideContours;
while (pq !== null) {
let q = iq || pq;
if (iq !== null) {
iq = iq.nextPeer;
} else {
pq = pq.nextPeer;
iq = pq && pq.insideContours;
}
switch (q.dir) {
case ContourDirection.CW: {
context.strokeStyle = 'red';
break;
}
case ContourDirection.CCW: {
context.strokeStyle = 'blue';
break;
}
case ContourDirection.Unknown: {
context.strokeStyle = 'green';
break;
}
}
let p = q.firstVertex;
context.beginPath();
context.moveTo(p.x, p.y);
do {
p = p.next;
context.lineTo(p.x, p.y);
} while (p !== q.firstVertex);
context.stroke();
}
}
}

@ -1,4 +1,3 @@
/* @preserve ASM BEGIN */
/* eslint-disable eqeqeq */
function Skeletonizer(stdlib, foreign, buffer) {
"use asm";
@ -198,10 +197,10 @@ function Skeletonizer(stdlib, foreign, buffer) {
done = ((sum | 0) == 0 | 0);
} while (!done);
}
return {
skeletonize: skeletonize
};
}
/* @preserve ASM END */
export default Skeletonizer;
/* eslint-enable eqeqeq */

@ -1,101 +0,0 @@
/**
* http://www.codeproject.com/Tips/407172/Connected-Component-Labeling-and-Vectorization
*/
var Tracer = {
searchDirections: [[0, 1], [1, 1], [1, 0], [1, -1], [0, -1], [-1, -1], [-1, 0], [-1, 1]],
create: function(imageWrapper, labelWrapper) {
var imageData = imageWrapper.data,
labelData = labelWrapper.data,
searchDirections = this.searchDirections,
width = imageWrapper.size.x,
pos;
function trace(current, color, label, edgelabel) {
var i,
y,
x;
for ( i = 0; i < 7; i++) {
y = current.cy + searchDirections[current.dir][0];
x = current.cx + searchDirections[current.dir][1];
pos = y * width + x;
if ((imageData[pos] === color) && ((labelData[pos] === 0) || (labelData[pos] === label))) {
labelData[pos] = label;
current.cy = y;
current.cx = x;
return true;
} else {
if (labelData[pos] === 0) {
labelData[pos] = edgelabel;
}
current.dir = (current.dir + 1) % 8;
}
}
return false;
}
function vertex2D(x, y, dir) {
return {
dir: dir,
x: x,
y: y,
next: null,
prev: null
};
}
function contourTracing(sy, sx, label, color, edgelabel) {
var Fv = null,
Cv,
P,
ldir,
current = {
cx: sx,
cy: sy,
dir: 0
};
if (trace(current, color, label, edgelabel)) {
Fv = vertex2D(sx, sy, current.dir);
Cv = Fv;
ldir = current.dir;
P = vertex2D(current.cx, current.cy, 0);
P.prev = Cv;
Cv.next = P;
P.next = null;
Cv = P;
do {
current.dir = (current.dir + 6) % 8;
trace(current, color, label, edgelabel);
if (ldir !== current.dir) {
Cv.dir = current.dir;
P = vertex2D(current.cx, current.cy, 0);
P.prev = Cv;
Cv.next = P;
P.next = null;
Cv = P;
} else {
Cv.dir = ldir;
Cv.x = current.cx;
Cv.y = current.cy;
}
ldir = current.dir;
} while (current.cx !== sx || current.cy !== sy);
Fv.prev = Cv.prev;
Cv.prev.next = Fv;
}
return Fv;
}
return {
trace: function(current, color, label, edgelabel) {
return trace(current, color, label, edgelabel);
},
contourTracing: function(sy, sx, label, color, edgelabel) {
return contourTracing(sy, sx, label, color, edgelabel);
}
};
}
};
export default (Tracer);

@ -0,0 +1,117 @@
import { ImageWrapper } from "../common/image-wrapper";
/**
* @borrows http://www.codeproject.com/Tips/407172/Connected-Component-Labeling-and-Vectorization
*/
type Direction = number;
export const SearchDirections: Array<Array<Direction>> = [[0, 1], [1, 1], [1, 0], [1, -1], [0, -1], [-1, -1], [-1, 0], [-1, 1]];
export interface ContourVertex {
x: number,
y: number,
dir: Direction,
next: ContourVertex,
prev: ContourVertex
}
interface Current {
cx: number,
cy: number,
dir: Direction
}
export class Tracer {
private _imageData: Uint8Array;
private _labelData: Array<number>;
private _width: number;
constructor(imageWrapper: ImageWrapper, labelWrapper: ImageWrapper<Array<number>>) {
this._imageData = imageWrapper.data;
this._labelData = labelWrapper.data as Array<number>;
this._width = imageWrapper.size.x;
}
trace(current: Current, color: number, label: number, edgeLabel: number): boolean {
for (let i = 0; i < 7; i++) {
const y = current.cy + SearchDirections[current.dir][0] | 0;
const x = current.cx + SearchDirections[current.dir][1] | 0;
const pos = y * this._width + x | 0;
if ((this._imageData[pos] === color) && ((this._labelData[pos] === 0) || (this._labelData[pos] === label))) {
this._labelData[pos] = label;
current.cx = x;
current.cy = y;
return true;
} else {
if (this._labelData[pos] === 0) {
this._labelData[pos] = edgeLabel;
}
current.dir = (current.dir + 1) % 8;
}
}
return false;
}
contourTracing(sy: number, sx: number, label: number, color: number, edgeLabel: number): ContourVertex {
let Fv: ContourVertex = null;
const current: Current = {
cx: sx,
cy: sy,
dir: 0
};
if (this.trace(current, color, label, edgeLabel)) {
Fv = {
x: sx,
y: sy,
dir: current.dir,
next: null,
prev: null
};
let Cv = Fv;
let ldir = current.dir;
let P = {
x: current.cx,
y: current.cy,
dir: 0,
next: null,
prev: Cv
};
Cv.next = P;
Cv = P;
do {
current.dir = (current.dir + 6) % 8;
this.trace(current, color, label, edgeLabel);
if (ldir !== current.dir) {
Cv.dir = current.dir;
P = {
x: current.cx,
y: current.cy,
dir: 0,
next: null,
prev: Cv
};
Cv.next = P;
Cv = P;
} else {
Cv.dir = ldir;
Cv.x = current.cx;
Cv.y = current.cy;
}
ldir = current.dir;
} while (current.cx !== sx || current.cy !== sy);
Fv.prev = Cv.prev;
Cv.prev.next = Fv;
}
return Fv;
}
}

@ -1,543 +0,0 @@
import TypeDefs from './common/typedefs'; // eslint-disable-line no-unused-vars
import ImageWrapper from './common/image_wrapper';
import BarcodeLocator from './locator/barcode_locator';
import BarcodeDecoder from './decoder/barcode_decoder';
import Events from './common/events';
import CameraAccess from './input/camera_access';
import ImageDebug from './common/image_debug';
import ResultCollector from './analytics/result_collector';
import Config from './config/config';
import InputStream from 'input_stream';
import FrameGrabber from 'frame_grabber';
import {merge} from 'lodash';
const vec2 = {
clone: require('gl-vec2/clone')
};
var _inputStream,
_framegrabber,
_stopped,
_canvasContainer = {
ctx: {
image: null,
overlay: null
},
dom: {
image: null,
overlay: null
}
},
_inputImageWrapper,
_boxSize,
_decoder,
_workerPool = [],
_onUIThread = true,
_resultCollector,
_config = {};
function initializeData(imageWrapper) {
initBuffers(imageWrapper);
_decoder = BarcodeDecoder.create(_config.decoder, _inputImageWrapper);
}
function initInputStream(cb) {
var video;
if (_config.inputStream.type === "VideoStream") {
video = document.createElement("video");
_inputStream = InputStream.createVideoStream(video);
} else if (_config.inputStream.type === "ImageStream") {
_inputStream = InputStream.createImageStream();
} else if (_config.inputStream.type === "LiveStream") {
var $viewport = getViewPort();
if ($viewport) {
video = $viewport.querySelector("video");
if (!video) {
video = document.createElement("video");
$viewport.appendChild(video);
}
}
_inputStream = InputStream.createLiveStream(video);
CameraAccess.request(video, _config.inputStream.constraints)
.then(() => {
_inputStream.trigger("canrecord");
}).catch((err) => {
return cb(err);
});
}
_inputStream.setAttribute("preload", "auto");
_inputStream.setInputStream(_config.inputStream);
_inputStream.addEventListener("canrecord", canRecord.bind(undefined, cb));
}
function getViewPort() {
var target = _config.inputStream.target;
// Check if target is already a DOM element
if (target && target.nodeName && target.nodeType === 1) {
return target;
} else {
// Use '#interactive.viewport' as a fallback selector (backwards compatibility)
var selector = typeof target === 'string' ? target : '#interactive.viewport';
return document.querySelector(selector);
}
}
function canRecord(cb) {
BarcodeLocator.checkImageConstraints(_inputStream, _config.locator);
initCanvas(_config);
_framegrabber = FrameGrabber.create(_inputStream, _canvasContainer.dom.image);
adjustWorkerPool(_config.numOfWorkers, function() {
if (_config.numOfWorkers === 0) {
initializeData();
}
ready(cb);
});
}
function ready(cb){
_inputStream.play();
cb();
}
function initCanvas() {
if (typeof document !== "undefined") {
var $viewport = getViewPort();
_canvasContainer.dom.image = document.querySelector("canvas.imgBuffer");
if (!_canvasContainer.dom.image) {
_canvasContainer.dom.image = document.createElement("canvas");
_canvasContainer.dom.image.className = "imgBuffer";
if ($viewport && _config.inputStream.type === "ImageStream") {
$viewport.appendChild(_canvasContainer.dom.image);
}
}
_canvasContainer.ctx.image = _canvasContainer.dom.image.getContext("2d");
_canvasContainer.dom.image.width = _inputStream.getCanvasSize().x;
_canvasContainer.dom.image.height = _inputStream.getCanvasSize().y;
_canvasContainer.dom.overlay = document.querySelector("canvas.drawingBuffer");
if (!_canvasContainer.dom.overlay) {
_canvasContainer.dom.overlay = document.createElement("canvas");
_canvasContainer.dom.overlay.className = "drawingBuffer";
if ($viewport) {
$viewport.appendChild(_canvasContainer.dom.overlay);
}
var clearFix = document.createElement("br");
clearFix.setAttribute("clear", "all");
if ($viewport) {
$viewport.appendChild(clearFix);
}
}
_canvasContainer.ctx.overlay = _canvasContainer.dom.overlay.getContext("2d");
_canvasContainer.dom.overlay.width = _inputStream.getCanvasSize().x;
_canvasContainer.dom.overlay.height = _inputStream.getCanvasSize().y;
}
}
function initBuffers(imageWrapper) {
if (imageWrapper) {
_inputImageWrapper = imageWrapper;
} else {
_inputImageWrapper = new ImageWrapper({
x: _inputStream.getWidth(),
y: _inputStream.getHeight()
});
}
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])
];
BarcodeLocator.init(_inputImageWrapper, _config.locator);
}
function getBoundingBoxes() {
if (_config.locate) {
return BarcodeLocator.locate();
} else {
return [[
vec2.clone(_boxSize[0]),
vec2.clone(_boxSize[1]),
vec2.clone(_boxSize[2]),
vec2.clone(_boxSize[3])]];
}
}
function transformResult(result) {
var topRight = _inputStream.getTopRight(),
xOffset = topRight.x,
yOffset = topRight.y,
i;
if (xOffset === 0 && yOffset === 0) {
return;
}
if (result.barcodes) {
for (i = 0; i < result.barcodes.length; i++) {
transformResult(result.barcodes[i]);
}
}
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, _inputStream.getCanvasSize(), result.codeResult);
}
}
function hasCodeResult (result) {
return result && (result.barcodes ?
result.barcodes.some(barcode => barcode.codeResult) :
result.codeResult);
}
function publishResult(result, imageData) {
let resultToPublish = result;
if (result && _onUIThread) {
transformResult(result);
addResult(result, imageData);
resultToPublish = result.barcodes || result;
}
Events.publish("processed", resultToPublish);
if (hasCodeResult(result)) {
Events.publish("detected", resultToPublish);
}
}
function locateAndDecode() {
var result,
boxes;
boxes = getBoundingBoxes();
if (boxes) {
result = _decoder.decodeFromBoundingBoxes(boxes);
result = result || {};
result.boxes = boxes;
publishResult(result, _inputImageWrapper.data);
} else {
publishResult();
}
}
function update() {
var availableWorker;
if (_onUIThread) {
if (_workerPool.length > 0) {
availableWorker = _workerPool.filter(function(workerThread) {
return !workerThread.busy;
})[0];
if (availableWorker) {
_framegrabber.attachData(availableWorker.imageData);
} else {
return; // all workers are busy
}
} else {
_framegrabber.attachData(_inputImageWrapper.data);
}
if (_framegrabber.grab()) {
if (availableWorker) {
availableWorker.busy = true;
availableWorker.worker.postMessage({
cmd: 'process',
imageData: availableWorker.imageData
}, [availableWorker.imageData.buffer]);
} else {
locateAndDecode();
}
}
} else {
locateAndDecode();
}
}
function startContinuousUpdate() {
var next = null,
delay = 1000 / (_config.frequency || 60);
_stopped = false;
(function frame(timestamp) {
next = next || timestamp;
if (!_stopped) {
if (timestamp >= next) {
next += delay;
update();
}
window.requestAnimFrame(frame);
}
}(performance.now()));
}
function start() {
if (_onUIThread && _config.inputStream.type === "LiveStream") {
startContinuousUpdate();
} else {
update();
}
}
function initWorker(cb) {
var blobURL,
workerThread = {
worker: undefined,
imageData: new Uint8Array(_inputStream.getWidth() * _inputStream.getHeight()),
busy: true
};
blobURL = generateWorkerBlob();
workerThread.worker = new Worker(blobURL);
workerThread.worker.onmessage = function(e) {
if (e.data.event === 'initialized') {
URL.revokeObjectURL(blobURL);
workerThread.busy = false;
workerThread.imageData = new Uint8Array(e.data.imageData);
if (ENV.development) {
console.log("Worker initialized");
}
return cb(workerThread);
} else if (e.data.event === 'processed') {
workerThread.imageData = new Uint8Array(e.data.imageData);
workerThread.busy = false;
publishResult(e.data.result, workerThread.imageData);
} else if (e.data.event === 'error') {
if (ENV.development) {
console.log("Worker error: " + e.data.message);
}
}
};
workerThread.worker.postMessage({
cmd: 'init',
size: {x: _inputStream.getWidth(), y: _inputStream.getHeight()},
imageData: workerThread.imageData,
config: configForWorker(_config)
}, [workerThread.imageData.buffer]);
}
function configForWorker(config) {
return {
...config,
inputStream: {
...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;
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));
Quagga.init(config, ready, imageWrapper);
Quagga.onProcessed(onProcessed);
} else if (e.data.cmd === 'process') {
imageWrapper.data = new Uint8Array(e.data.imageData);
Quagga.start();
} else if (e.data.cmd === 'setReaders') {
Quagga.setReaders(e.data.readers);
}
};
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 setReaders(readers) {
if (_decoder) {
_decoder.setReaders(readers);
} else if (_onUIThread && _workerPool.length > 0) {
_workerPool.forEach(function(workerThread) {
workerThread.worker.postMessage({cmd: 'setReaders', readers: readers});
});
}
}
function adjustWorkerPool(capacity, cb) {
const increaseBy = capacity - _workerPool.length;
if (increaseBy === 0) {
return cb && cb();
}
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 cb && cb();
} else {
for (var i = 0; i < increaseBy; i++) {
initWorker(workerInitialized);
}
function workerInitialized(workerThread) {
_workerPool.push(workerThread);
if (_workerPool.length >= capacity){
cb && cb();
}
}
}
}
export default {
init: function(config, cb, imageWrapper) {
_config = merge({}, Config, config);
if (imageWrapper) {
_onUIThread = false;
initializeData(imageWrapper);
return cb();
} else {
initInputStream(cb);
}
},
start: function() {
start();
},
stop: function() {
_stopped = true;
adjustWorkerPool(0);
if (_config.inputStream.type === "LiveStream") {
CameraAccess.release();
_inputStream.clearEventHandlers();
}
},
pause: function() {
_stopped = true;
},
onDetected: function(callback) {
Events.subscribe("detected", callback);
},
offDetected: function(callback) {
Events.unsubscribe("detected", callback);
},
onProcessed: function(callback) {
Events.subscribe("processed", callback);
},
offProcessed: function(callback) {
Events.unsubscribe("processed", callback);
},
setReaders: function(readers) {
setReaders(readers);
},
registerResultCollector: function(resultCollector) {
if (resultCollector && typeof resultCollector.addResult === 'function') {
_resultCollector = resultCollector;
}
},
canvas: _canvasContainer,
decodeSingle: function(config, resultCallback) {
config = merge({
inputStream: {
type: "ImageStream",
sequence: false,
size: 800,
src: config.src
},
numOfWorkers: (ENV.development && config.debug) ? 0 : 1,
locator: {
halfSample: false
}
}, config);
this.init(config, () => {
Events.once("processed", (result) => {
this.stop();
resultCallback.call(null, result);
}, true);
start();
});
},
ImageWrapper: ImageWrapper,
ImageDebug: ImageDebug,
ResultCollector: ResultCollector,
CameraAccess: CameraAccess,
};

@ -0,0 +1,489 @@
import _polyfills from './common/polyfills';
import { ResultCollector } from './analytics/result-collector';
import { Box } from './common/box';
import { EventCallback, Events, EventSubscription } from './common/events';
import { ImageDebug } from './common/image-debug';
import { ImageWrapper } from './common/image-wrapper';
import { merge } from './common/merge';
import { Point } from './common/point';
import { config as defaultConfig, QuaggaConfig } from './config/config';
import { BarcodeDecoder, QuaggaBarcode } from './decoder/barcode-decoder';
import { CameraAccess } from './input/camera-access';
import { FrameGrabber } from './input/frame-grabber';
import { ImageStream } from './input/image-stream';
import { InputStream } from './input/input-stream';
import { LiveStream } from './input/live-stream';
import { VideoStream } from './input/video-stream';
import { checkImageConstraints } from './input/input-stream-utils';
import { BarcodeLocator } from './locator/barcode-locator';
import { BarcodeReaderDeclaration } from './reader/barcode-reader';
interface WorkerThread {
worker: Worker;
imageData: Uint8Array;
busy: boolean;
}
export interface QuaggaCanvasContainer {
ctx: {
image: CanvasRenderingContext2D,
overlay: CanvasRenderingContext2D
},
dom: {
image: HTMLCanvasElement,
overlay: HTMLCanvasElement
}
}
let _inputStream: InputStream;
let _frameGrabber: FrameGrabber;
let _stopped: boolean;
const _canvasContainer: QuaggaCanvasContainer = {
ctx: {
image: null,
overlay: null
},
dom: {
image: null,
overlay: null
}
};
let _inputImageWrapper: ImageWrapper;
let _locator: BarcodeLocator;
let _boxSize: Box;
let _decoder: BarcodeDecoder;
let _workerPool = new Array<WorkerThread>();
let _onUIThread: boolean;
let _resultCollector: ResultCollector;
let _config: QuaggaConfig;
export default {
init(config: QuaggaConfig, cb: () => void, imageWrapper?: ImageWrapper) {
_onUIThread = true;
_config = merge(defaultConfig, config);
if (imageWrapper) {
_onUIThread = false;
_initializeData(imageWrapper);
cb();
} else {
_initInputStream(cb);
}
},
CameraAccess: CameraAccess,
ImageDebug: ImageDebug,
ImageWrapper: ImageWrapper,
ResultCollector: ResultCollector,
get canvas(): QuaggaCanvasContainer {
return _canvasContainer;
},
start(): void {
if (_onUIThread && _config.inputStream.type === 'LiveStream') {
_startContinuousUpdate();
} else {
_update();
}
},
stop(): void {
_stopped = true;
_adjustWorkerPool(0);
if (_config.inputStream.type === 'LiveStream') {
CameraAccess.release();
_inputStream.clearEventHandlers();
}
},
decodeSingle(config: QuaggaConfig, resultCallback: (_: QuaggaBarcode) => void): void {
config = merge({
inputStream: {
type: 'ImageStream',
sequence: false,
size: 800,
src: config.src
},
numOfWorkers: (process.env.NODE_ENV !== 'production' && config.debug) ? 0 : 1,
locator: {
halfSample: false
}
}, config);
this.init(config, () => {
Events.once('processed', (result: QuaggaBarcode) => {
this.stop();
resultCallback.call(null, result);
}, true);
this.start();
});
},
pause(): void {
_stopped = true;
},
onDetected(callback: EventSubscription | EventCallback): void {
Events.subscribe('detected', callback);
},
offDetected(callback: EventCallback): void {
Events.unsubscribe('detected', callback);
},
onProcessed(callback: EventSubscription | EventCallback): void {
Events.subscribe('processed', callback);
},
offProcessed(callback: EventCallback): void {
Events.unsubscribe('processed', callback);
},
setReaders(readers: Array<BarcodeReaderDeclaration>): void {
if (_decoder) {
_decoder.setReaders(readers);
} else if (_onUIThread && _workerPool.length > 0) {
_workerPool.forEach(({ worker }) => worker.postMessage({ cmd: 'setReaders', readers }));
}
},
registerResultCollector(resultCollector: ResultCollector): void {
if (resultCollector && typeof resultCollector.addResult === 'function') {
_resultCollector = resultCollector;
}
}
};
function _initializeData(imageWrapper?: ImageWrapper): void {
_initBuffers(imageWrapper);
_decoder = new BarcodeDecoder(_config.decoder, _inputImageWrapper);
}
function _initInputStream(callback: (err?: any) => void): void {
let video: HTMLVideoElement;
if (_config.inputStream.type === 'VideoStream') {
video = document.createElement('video');
_inputStream = new VideoStream(video);
} else if (_config.inputStream.type === 'ImageStream') {
_inputStream = new ImageStream();
} else if (_config.inputStream.type === 'LiveStream') {
const viewport = _getViewPort();
if (viewport) {
video = viewport.querySelector('video');
if (!video) {
video = document.createElement('video');
viewport.appendChild(video);
}
}
_inputStream = new LiveStream(video);
CameraAccess.request(video, _config.inputStream.constraints)
.then(() => _inputStream.trigger('canrecord'), err => callback(err));
}
_inputStream.setAttribute('preload', 'auto');
_inputStream.config = _config.inputStream;
_inputStream.addEventListener('canrecord', _canRecord.bind(this, callback));
}
function _getViewPort(): HTMLElement {
const target = _config.inputStream.target;
// Check if target is already a DOM element
if (target instanceof HTMLElement) {
return target;
} else {
// Use '#interactive.viewport' as a fallback selector (backwards compatibility)
const selector = typeof target === 'string' ? target : '#interactive.viewport';
return document.querySelector(selector);
}
}
function _canRecord(cb: () => void): void {
checkImageConstraints(_inputStream, _config.locator);
_initCanvas();
_frameGrabber = new FrameGrabber(_inputStream, _canvasContainer.dom.image);
_adjustWorkerPool(_config.numOfWorkers, () => {
if (_config.numOfWorkers === 0) {
_initializeData();
}
_inputStream.play();
cb();
});
}
function _initCanvas(): void {
if (typeof document !== 'undefined') {
const viewport = _getViewPort();
_canvasContainer.dom.image = document.querySelector('canvas.imgBuffer');
if (!_canvasContainer.dom.image) {
_canvasContainer.dom.image = document.createElement('canvas');
_canvasContainer.dom.image.className = 'imgBuffer';
if (viewport && _config.inputStream.type === 'ImageStream') {
viewport.appendChild(_canvasContainer.dom.image);
}
}
_canvasContainer.ctx.image = _canvasContainer.dom.image.getContext('2d');
_canvasContainer.dom.image.width = _inputStream.canvasWidth;
_canvasContainer.dom.image.height = _inputStream.canvasHeight;
_canvasContainer.dom.overlay = document.querySelector('canvas.drawingBuffer');
if (!_canvasContainer.dom.overlay) {
_canvasContainer.dom.overlay = document.createElement('canvas');
_canvasContainer.dom.overlay.className = 'drawingBuffer';
if (viewport) {
viewport.appendChild(_canvasContainer.dom.overlay);
}
const clearFix = document.createElement('br');
clearFix.setAttribute('clear', 'all');
if (viewport) {
viewport.appendChild(clearFix);
}
}
_canvasContainer.ctx.overlay = _canvasContainer.dom.overlay.getContext('2d');
_canvasContainer.dom.overlay.width = _inputStream.canvasWidth;
_canvasContainer.dom.overlay.height = _inputStream.canvasHeight;
}
}
function _initBuffers(imageWrapper?: ImageWrapper): void {
if (imageWrapper) {
_inputImageWrapper = imageWrapper;
} else {
_inputImageWrapper = new ImageWrapper({
x: _inputStream.width,
y: _inputStream.height
});
}
if (process.env.NODE_ENV !== 'production') {
console.log(_inputImageWrapper.size);
}
_boxSize = [
{ x: 0, y: 0 },
{ x: 0, y: _inputImageWrapper.size.y },
{ x: _inputImageWrapper.size.x, y: _inputImageWrapper.size.y },
{ x: _inputImageWrapper.size.x, y: 0 }
];
_locator = new BarcodeLocator(_inputImageWrapper, _config.locator);
}
function _transform(polygon: ReadonlyArray<Point>, offset: Point): void {
polygon.forEach(vertex => {
vertex.x += offset.x;
vertex.y += offset.y;
})
}
function _transformResult(result: QuaggaBarcode, offset: Point): void {
if (result.barcodes) {
result.barcodes.forEach(barcode => _transformResult(barcode, offset));
}
if (result.line) {
_transform(result.line, offset);
}
if (result.box) {
_transform(result.box, offset);
}
if (result.boxes) {
result.boxes.forEach(box => _transform(box, offset));
}
}
function _addResult(result: QuaggaBarcode, imageData: Uint8Array, canvasWidth: number, canvasHeight: number): void {
if (imageData && _resultCollector) {
if (result.barcodes) {
result.barcodes.forEach(({ codeResult }) => {
if (codeResult) {
_resultCollector.addResult(imageData, canvasWidth, canvasHeight, codeResult)
}
});
} else if (result.codeResult) {
_resultCollector.addResult(imageData, canvasWidth, canvasHeight, result.codeResult);
}
}
}
function _hasCodeResult(result: QuaggaBarcode): boolean {
return result && (!!result.codeResult || result.barcodes && result.barcodes.some(barcode => !!barcode.codeResult));
}
function _publishResult(result?: QuaggaBarcode, imageData?: Uint8Array): void {
let resultToPublish: QuaggaBarcode | Array<QuaggaBarcode> = result;
if (result && _onUIThread) {
const offset = _inputStream.topLeft;
if (offset.x !== 0 || offset.y !== 0) {
_transformResult(result, offset);
}
_addResult(result, imageData, _inputStream.canvasWidth, _inputStream.canvasHeight);
resultToPublish = result.barcodes || result;
}
Events.publish('processed', resultToPublish);
if (_hasCodeResult(result)) {
Events.publish('detected', resultToPublish);
}
}
function _locateAndDecode(): void {
const boxes = _config.locate ? _locator.locate() : [_boxSize];
const result = _decoder.decodeFromBoundingBoxes(boxes);
_publishResult(result, _inputImageWrapper.data);
}
function _update(): void {
if (_onUIThread) {
if (_workerPool.length > 0) {
const availableWorker = _workerPool.find(({ busy }) => !busy);
if (!availableWorker) {
return; // all workers are busy
}
const imageData = availableWorker.imageData;
if (_frameGrabber.grab(imageData)) {
availableWorker.busy = true;
availableWorker.worker.postMessage({ cmd: 'process', imageData }, [imageData.buffer]);
}
} else if (_frameGrabber.grab(_inputImageWrapper.data)) {
_locateAndDecode();
}
} else {
_locateAndDecode();
}
}
function _startContinuousUpdate(): void {
const delay = 1000 / (_config.frequency || 60);
let next = null;
_stopped = false;
(function frame(timestamp): void {
next = next || timestamp;
if (!_stopped) {
if (timestamp >= next) {
next += delay;
_update();
}
window.requestAnimationFrame(frame);
}
}(performance.now()));
}
function _initWorker(cb: (workerThread: WorkerThread) => void): void {
const blobURL = _generateWorkerBlob();
const workerThread = {
worker: new Worker(blobURL),
imageData: new Uint8Array(_inputStream.width * _inputStream.height),
busy: true
};
workerThread.worker.onmessage = ({ data }) => {
if (data.event === 'initialized') {
URL.revokeObjectURL(blobURL);
workerThread.busy = false;
workerThread.imageData = new Uint8Array(data.imageData);
if (process.env.NODE_ENV !== 'production') {
console.log('Worker initialized');
}
cb(workerThread);
} else if (data.event === 'processed') {
workerThread.busy = false;
workerThread.imageData = new Uint8Array(data.imageData);
_publishResult(data.result, workerThread.imageData);
} else if (data.event === 'error') {
if (process.env.NODE_ENV !== 'production') {
console.log('Worker error:', data.message);
}
}
};
workerThread.worker.postMessage({
cmd: 'init',
size: { x: _inputStream.width, y: _inputStream.height },
imageData: workerThread.imageData,
config: merge(_config, { inputStream: { target: null } })
}, [workerThread.imageData.buffer]);
}
function _workerInterface(factory: Function): void {
let Quagga: any;
const worker: any = self;
let imageWrapper: ImageWrapper;
if (factory) {
Quagga = factory().default;
if (!Quagga) {
worker.postMessage({ event: 'error', message: 'Quagga could not be created' });
return;
}
}
self.onmessage = ({ data }) => {
if (data.cmd === 'init') {
const config: QuaggaConfig = data.config;
config.numOfWorkers = 0;
imageWrapper = new Quagga.ImageWrapper({ x: data.size.x, y: data.size.y }, new Uint8Array(data.imageData));
Quagga.init(
config,
() => worker.postMessage(
{ event: 'initialized', imageData: imageWrapper.data }, [imageWrapper.data.buffer]
),
imageWrapper
);
Quagga.onProcessed((result: QuaggaBarcode) =>
worker.postMessage(
{ event: 'processed', imageData: imageWrapper.data, result }, [imageWrapper.data.buffer]
)
);
} else if (data.cmd === 'process') {
imageWrapper.data = new Uint8Array(data.imageData);
Quagga.start();
} else if (data.cmd === 'setReaders') {
Quagga.setReaders(data.readers);
}
};
}
function _generateWorkerBlob(): string {
// @ts-ignore
let factorySource: string = __factorySource__ || '';
const blob = new Blob([`(${_workerInterface.toString()})(${factorySource});`], { type: 'text/javascript' });
return window.URL.createObjectURL(blob);
}
function _adjustWorkerPool(capacity: number, cb?: () => void): void {
const increaseBy = capacity - _workerPool.length;
if (increaseBy > 0) {
for (let i = 0; i < increaseBy; i++) {
_initWorker(workerThread => {
_workerPool.push(workerThread);
if (_workerPool.length >= capacity && cb) {
cb();
}
});
}
} else {
if (increaseBy < 0) {
_workerPool.slice(increaseBy).forEach(({ worker }) => {
worker.terminate();
if (process.env.NODE_ENV !== 'production') {
console.log('Worker terminated!');
}
});
_workerPool = _workerPool.slice(0, increaseBy);
}
return cb && cb();
}
}

@ -0,0 +1,177 @@
import { Barcode, BarcodeInfo, BarcodeReader, BarcodeReaderConfig } from './barcode-reader';
const N = 1;
const W = 3;
const START_PATTERN = [W, N, W, N, N, N];
const STOP_PATTERN = [W, N, N, N, W];
const CODE_PATTERN = [
[N, N, W, W, N],
[W, N, N, N, W],
[N, W, N, N, W],
[W, W, N, N, N],
[N, N, W, N, W],
[W, N, W, N, N],
[N, W, W, N, N],
[N, N, N, W, W],
[W, N, N, W, N],
[N, W, N, W, N]
];
const startPatternLength = START_PATTERN.reduce((sum, val) => sum + val, 0);
export class TwoOfFiveReader extends BarcodeReader {
private _barSpaceRatio: [number, number];
constructor(config?: BarcodeReaderConfig) {
super(config);
this._barSpaceRatio = [1, 1];
this._format = '2of5';
this._singleCodeError = 0.78;
this._averageCodeError = 0.30;
}
decode(): Barcode {
const startInfo = this._findStart();
if (!startInfo) {
return null;
}
const endInfo = this._findEnd();
if (!endInfo) {
return null;
}
const counters = this._fillCounters(startInfo.end, endInfo.start, 0);
if (counters.length % 10 !== 0) {
return null;
}
const result = new Array<number>();
const decodedCodes = new Array<BarcodeInfo>();
decodedCodes.push(startInfo);
const code = this._decodePayload(counters, result, decodedCodes);
if (!code || result.length < 5) {
return null;
}
decodedCodes.push(endInfo);
return {
code: result.join(''),
start: startInfo.start,
end: endInfo.end,
startInfo,
decodedCodes
};
}
protected _findStart(): BarcodeInfo {
let offset = this._nextSet(this._row);
let narrowBarWidth = 1;
let startInfo: BarcodeInfo;
while (!startInfo) {
startInfo = this._findPattern(START_PATTERN, offset, 0, true);
if (!startInfo) {
return null;
}
narrowBarWidth = (startInfo.end - startInfo.start) / startPatternLength | 0;
const leadingWhitespaceStart = startInfo.start - narrowBarWidth * 5;
if (leadingWhitespaceStart >= 0) {
if (this._matchRange(leadingWhitespaceStart, startInfo.start, 0)) {
return startInfo;
}
}
offset = startInfo.end;
startInfo = null;
}
return null;
}
protected _verifyTrailingWhitespace(endInfo: BarcodeInfo): BarcodeInfo {
const trailingWhitespaceEnd = endInfo.end + (endInfo.end - endInfo.start) / 2;
if (trailingWhitespaceEnd < this._row.length) {
if (this._matchRange(endInfo.end, trailingWhitespaceEnd, 0)) {
return endInfo;
}
}
return null;
}
protected _findEnd(): BarcodeInfo {
this._row.reverse();
const offset = this._nextSet(this._row);
const endInfo = this._findPattern(STOP_PATTERN, offset, 0, true);
this._row.reverse();
if (endInfo === null) {
return null;
}
// reverse numbers
const start = endInfo.start;
endInfo.start = this._row.length - endInfo.end;
endInfo.end = this._row.length - start;
return endInfo !== null ? this._verifyTrailingWhitespace(endInfo) : null;
}
protected _decodeCode(counter: ReadonlyArray<number>): BarcodeInfo {
const bestMatch: BarcodeInfo = {
error: Number.MAX_VALUE,
code: -1,
start: 0,
end: 0
};
for (let code = 0; code < CODE_PATTERN.length; code++) {
const error = this._matchPattern(counter, CODE_PATTERN[code]);
if (error < bestMatch.error) {
bestMatch.code = code;
bestMatch.error = error;
}
}
return bestMatch.error < this.AVERAGE_CODE_ERROR ? bestMatch : null;
}
protected _decodePayload(counters: ReadonlyArray<number>, result: Array<number>, decodedCodes: Array<BarcodeInfo>): BarcodeInfo {
const counterLength = counters.length;
const counter = [0, 0, 0, 0, 0];
let pos = 0;
let code: BarcodeInfo;
while (pos < counterLength) {
for (let i = 0; i < 5; i++) {
counter[i] = counters[pos] * this._barSpaceRatio[0];
pos += 2;
}
code = this._decodeCode(counter);
if (!code) {
return null;
}
result.push(code.code);
decodedCodes.push(code);
}
return code;
}
}

@ -1,257 +0,0 @@
import BarcodeReader from './barcode_reader';
function TwoOfFiveReader(opts) {
BarcodeReader.call(this, opts);
this.barSpaceRatio = [1, 1];
}
var N = 1,
W = 3,
properties = {
START_PATTERN: {value: [W, N, W, N, N, N]},
STOP_PATTERN: {value: [W, N, N, N, W]},
CODE_PATTERN: {value: [
[N, N, W, W, N],
[W, N, N, N, W],
[N, W, N, N, W],
[W, W, N, N, N],
[N, N, W, N, W],
[W, N, W, N, N],
[N, W, W, N, N],
[N, N, N, W, W],
[W, N, N, W, N],
[N, W, N, W, N]
]},
SINGLE_CODE_ERROR: {value: 0.78, writable: true},
AVG_CODE_ERROR: {value: 0.30, writable: true},
FORMAT: {value: "2of5"}
};
const startPatternLength = properties.START_PATTERN.value.reduce((sum, val) => sum + val, 0);
TwoOfFiveReader.prototype = Object.create(BarcodeReader.prototype, properties);
TwoOfFiveReader.prototype.constructor = TwoOfFiveReader;
TwoOfFiveReader.prototype._findPattern = function(pattern, offset, isWhite, tryHarder) {
var counter = [],
self = this,
i,
counterPos = 0,
bestMatch = {
error: Number.MAX_VALUE,
code: -1,
start: 0,
end: 0
},
error,
j,
sum,
epsilon = self.AVG_CODE_ERROR;
isWhite = isWhite || false;
tryHarder = tryHarder || false;
if (!offset) {
offset = self._nextSet(self._row);
}
for ( i = 0; i < pattern.length; i++) {
counter[i] = 0;
}
for ( i = offset; i < self._row.length; i++) {
if (self._row[i] ^ isWhite) {
counter[counterPos]++;
} else {
if (counterPos === counter.length - 1) {
sum = 0;
for ( j = 0; j < counter.length; j++) {
sum += counter[j];
}
error = self._matchPattern(counter, pattern);
if (error < epsilon) {
bestMatch.error = error;
bestMatch.start = i - sum;
bestMatch.end = i;
return bestMatch;
}
if (tryHarder) {
for (j = 0; j < counter.length - 2; j++) {
counter[j] = counter[j + 2];
}
counter[counter.length - 2] = 0;
counter[counter.length - 1] = 0;
counterPos--;
} else {
return null;
}
} else {
counterPos++;
}
counter[counterPos] = 1;
isWhite = !isWhite;
}
}
return null;
};
TwoOfFiveReader.prototype._findStart = function() {
var self = this,
leadingWhitespaceStart,
offset = self._nextSet(self._row),
startInfo,
narrowBarWidth = 1;
while (!startInfo) {
startInfo = self._findPattern(self.START_PATTERN, offset, false, true);
if (!startInfo) {
return null;
}
narrowBarWidth = Math.floor((startInfo.end - startInfo.start) / startPatternLength);
leadingWhitespaceStart = startInfo.start - narrowBarWidth * 5;
if (leadingWhitespaceStart >= 0) {
if (self._matchRange(leadingWhitespaceStart, startInfo.start, 0)) {
return startInfo;
}
}
offset = startInfo.end;
startInfo = null;
}
};
TwoOfFiveReader.prototype._verifyTrailingWhitespace = function(endInfo) {
var self = this,
trailingWhitespaceEnd;
trailingWhitespaceEnd = endInfo.end + ((endInfo.end - endInfo.start) / 2);
if (trailingWhitespaceEnd < self._row.length) {
if (self._matchRange(endInfo.end, trailingWhitespaceEnd, 0)) {
return endInfo;
}
}
return null;
};
TwoOfFiveReader.prototype._findEnd = function() {
var self = this,
endInfo,
tmp,
offset;
self._row.reverse();
offset = self._nextSet(self._row);
endInfo = self._findPattern(self.STOP_PATTERN, offset, false, true);
self._row.reverse();
if (endInfo === null) {
return null;
}
// reverse numbers
tmp = endInfo.start;
endInfo.start = self._row.length - endInfo.end;
endInfo.end = self._row.length - tmp;
return endInfo !== null ? self._verifyTrailingWhitespace(endInfo) : null;
};
TwoOfFiveReader.prototype._decodeCode = function(counter) {
var j,
self = this,
sum = 0,
normalized,
error,
epsilon = self.AVG_CODE_ERROR,
code,
bestMatch = {
error: Number.MAX_VALUE,
code: -1,
start: 0,
end: 0
};
for ( j = 0; j < counter.length; j++) {
sum += counter[j];
}
for (code = 0; code < self.CODE_PATTERN.length; code++) {
error = self._matchPattern(counter, self.CODE_PATTERN[code]);
if (error < bestMatch.error) {
bestMatch.code = code;
bestMatch.error = error;
}
}
if (bestMatch.error < epsilon) {
return bestMatch;
}
};
TwoOfFiveReader.prototype._decodePayload = function(counters, result, decodedCodes) {
var i,
self = this,
pos = 0,
counterLength = counters.length,
counter = [0, 0, 0, 0, 0],
code;
while (pos < counterLength) {
for (i = 0; i < 5; i++) {
counter[i] = counters[pos] * this.barSpaceRatio[0];
pos += 2;
}
code = self._decodeCode(counter);
if (!code) {
return null;
}
result.push(code.code + "");
decodedCodes.push(code);
}
return code;
};
TwoOfFiveReader.prototype._verifyCounterLength = function(counters) {
return (counters.length % 10 === 0);
};
TwoOfFiveReader.prototype._decode = function() {
var startInfo,
endInfo,
self = this,
code,
result = [],
decodedCodes = [],
counters;
startInfo = self._findStart();
if (!startInfo) {
return null;
}
decodedCodes.push(startInfo);
endInfo = self._findEnd();
if (!endInfo) {
return null;
}
counters = self._fillCounters(startInfo.end, endInfo.start, false);
if (!self._verifyCounterLength(counters)) {
return null;
}
code = self._decodePayload(counters, result, decodedCodes);
if (!code) {
return null;
}
if (result.length < 5) {
return null;
}
decodedCodes.push(endInfo);
return {
code: result.join(""),
start: startInfo.start,
end: endInfo.end,
startInfo: startInfo,
decodedCodes: decodedCodes
};
};
export default TwoOfFiveReader;

@ -0,0 +1,274 @@
export enum BarcodeDirection {
Forward = 1,
Reverse = -1
};
export type BarcodeFormat = string;
export type BarcodeReaderType = string;
export type BarcodeReaderDeclaration = BarcodeReaderType | { format: BarcodeReaderType; config: BarcodeReaderConfig; };
export interface BarcodeReaderConfig {
normalizeBarSpaceWidth?: boolean;
supplements?: Array<BarcodeReaderType>;
}
export interface BarcodeCorrection {
bar: number;
space: number;
}
export interface BarcodeInfo {
code?: number;
correction?: BarcodeCorrection;
end?: number;
endCounter?: number;
error?: number;
start?: number;
startCounter?: number;
}
export interface Barcode {
code?: string;
codeset?: number;
correction?: BarcodeCorrection;
decodedCodes?: Array<string | BarcodeInfo>;
direction?: BarcodeDirection;
end?: number;
endInfo?: BarcodeInfo;
format?: BarcodeFormat;
start?: number;
startInfo?: BarcodeInfo;
supplement?: Barcode;
}
export abstract class BarcodeReader {
protected _singleCodeError: number;
protected _averageCodeError: number;
protected _format: BarcodeFormat;
protected _row: Array<number>;
config: BarcodeReaderConfig;
supplements: Array<BarcodeReader>;
static get Exception() {
return {
StartNotFoundException: 'Start-Info was not found!',
CodeNotFoundException: 'Code could not be found!',
PatternNotFoundException: 'Pattern could not be found!'
};
}
get SINGLE_CODE_ERROR(): number {
return this._singleCodeError;
}
get AVERAGE_CODE_ERROR(): number {
return this._averageCodeError;
}
get FORMAT(): BarcodeFormat {
return this._format;
}
constructor(config?: BarcodeReaderConfig, supplements?: Array<BarcodeReader>) {
this._format = 'unknown';
this._row = [];
this.config = config || {};
this.supplements = supplements;
}
abstract decode(row?: Array<number>, start?: number): Barcode;
protected _findPattern(pattern: ReadonlyArray<number>, offset: number, isWhite: 0 | 1, tryHarder: boolean): BarcodeInfo {
const counter = new Array<number>(pattern.length);
const bestMatch: BarcodeInfo = {
error: Number.MAX_VALUE,
code: -1,
start: 0,
end: 0
};
const epsilon = this.AVERAGE_CODE_ERROR;
let counterPos = 0;
if (!offset) {
offset = this._nextSet(this._row);
}
counter.fill(0);
for (let i = offset; i < this._row.length; i++) {
if (this._row[i] ^ isWhite) {
counter[counterPos]++;
} else {
if (counterPos === counter.length - 1) {
const error = this._matchPattern(counter, pattern);
if (error < epsilon) {
bestMatch.error = error;
bestMatch.start = i - counter.reduce((sum, value) => sum + value, 0);
bestMatch.end = i;
return bestMatch;
}
if (tryHarder) {
for (let j = 0; j < counter.length - 2; j++) {
counter[j] = counter[j + 2];
}
counter[counter.length - 2] = 0;
counter[counter.length - 1] = 0;
counterPos--;
} else {
return null;
}
} else {
counterPos++;
}
counter[counterPos] = 1;
isWhite = isWhite ? 0 : 1;
}
}
return null;
}
protected _nextUnset(line: ReadonlyArray<number>, start?: number): number {
for (let i = start || 0; i < line.length; i++) {
if (!line[i]) {
return i;
}
}
return line.length;
}
protected _nextSet(line: ReadonlyArray<number>, start?: number): number {
for (let i = start || 0; i < line.length; i++) {
if (line[i]) {
return i;
}
}
return line.length;
}
protected _matchRange(start: number, end: number, value: number): boolean {
for (let i = start < 0 ? 0 : start; i < end; i++) {
if (this._row[i] !== value) {
return false;
}
}
return true;
}
protected _matchPattern(counter: ReadonlyArray<number>, code: ReadonlyArray<number>, maxSingleError?: number): number {
let error = 0;
let sum = 0;
let modulo = 0;
maxSingleError = maxSingleError || this.SINGLE_CODE_ERROR || 1;
for (let i = 0; i < counter.length; i++) {
sum += counter[i];
modulo += code[i];
}
if (sum < modulo) {
return Number.MAX_VALUE;
}
const barWidth = sum / modulo;
maxSingleError *= barWidth;
for (let i = 0; i < counter.length; i++) {
const count = counter[i];
const scaled = code[i] * barWidth;
const singleError = Math.abs(count - scaled) / scaled;
if (singleError > maxSingleError) {
return Number.MAX_VALUE;
}
error += singleError;
}
return error / modulo;
}
protected _correctBars(counter: Array<number>, correction: number, indices: Array<number>) {
let length = indices.length;
let tmp = 0;
while (length--) {
tmp = counter[indices[length]] * (1 - ((1 - correction) / 2));
if (tmp > 1) {
counter[indices[length]] = tmp;
}
}
}
decodePattern(pattern: Array<number>): Barcode {
this._row = pattern;
let result = this.decode();
if (result === null) {
this._row.reverse();
result = this.decode();
if (result) {
result.direction = BarcodeDirection.Reverse;
result.start = this._row.length - result.start;
result.end = this._row.length - result.end;
}
} else {
result.direction = BarcodeDirection.Forward;
}
if (result) {
result.format = this.FORMAT;
}
return result;
}
_fillCounters(offset: number, end: number, isWhite: 0 | 1): Array<number> {
const counters = new Array<number>();
let counterPos = 0;
counters[counterPos] = 0;
for (let i = offset; i < end; i++) {
if (this._row[i] ^ isWhite) {
counters[counterPos]++;
} else {
counterPos++;
counters[counterPos] = 1;
isWhite = isWhite ? 0 : 1;
}
}
return counters;
}
protected _toCounters(start: number, counters: Uint16Array): Uint16Array {
const numCounters = counters.length;
const end = this._row.length;
let isWhite: 0 | 1 = this._row[start] ? 0 : 1;
let counterPos = 0;
counters.fill(0);
for (let i = start; i < end; i++) {
if (this._row[i] ^ isWhite) {
counters[counterPos]++;
} else {
counterPos++;
if (counterPos === numCounters) {
break;
} else {
counters[counterPos] = 1;
isWhite = isWhite ? 0 : 1;
}
}
}
return counters;
}
}

@ -1,245 +0,0 @@
import ArrayHelper from '../common/array_helper';
function BarcodeReader(config, supplements) {
this._row = [];
this.config = config || {};
this.supplements = supplements;
return this;
}
BarcodeReader.prototype._nextUnset = function(line, start) {
var i;
if (start === undefined) {
start = 0;
}
for (i = start; i < line.length; i++) {
if (!line[i]) {
return i;
}
}
return line.length;
};
BarcodeReader.prototype._matchPattern = function(counter, code, maxSingleError) {
var i,
error = 0,
singleError = 0,
sum = 0,
modulo = 0,
barWidth,
count,
scaled;
maxSingleError = maxSingleError || this.SINGLE_CODE_ERROR || 1;
for (i = 0; i < counter.length; i++) {
sum += counter[i];
modulo += code[i];
}
if (sum < modulo) {
return Number.MAX_VALUE;
}
barWidth = sum / modulo;
maxSingleError *= barWidth;
for (i = 0; i < counter.length; i++) {
count = counter[i];
scaled = code[i] * barWidth;
singleError = Math.abs(count - scaled) / scaled;
if (singleError > maxSingleError) {
return Number.MAX_VALUE;
}
error += singleError;
}
return error / modulo;
};
BarcodeReader.prototype._nextSet = function(line, offset) {
var i;
offset = offset || 0;
for (i = offset; i < line.length; i++) {
if (line[i]) {
return i;
}
}
return line.length;
};
BarcodeReader.prototype._correctBars = function(counter, correction, indices) {
var length = indices.length,
tmp = 0;
while(length--) {
tmp = counter[indices[length]] * (1 - ((1 - correction) / 2));
if (tmp > 1) {
counter[indices[length]] = tmp;
}
}
}
BarcodeReader.prototype._matchTrace = function(cmpCounter, epsilon) {
var counter = [],
i,
self = this,
offset = self._nextSet(self._row),
isWhite = !self._row[offset],
counterPos = 0,
bestMatch = {
error: Number.MAX_VALUE,
code: -1,
start: 0
},
error;
if (cmpCounter) {
for ( i = 0; i < cmpCounter.length; i++) {
counter.push(0);
}
for ( i = offset; i < self._row.length; i++) {
if (self._row[i] ^ isWhite) {
counter[counterPos]++;
} else {
if (counterPos === counter.length - 1) {
error = self._matchPattern(counter, cmpCounter);
if (error < epsilon) {
bestMatch.start = i - offset;
bestMatch.end = i;
bestMatch.counter = counter;
return bestMatch;
} else {
return null;
}
} else {
counterPos++;
}
counter[counterPos] = 1;
isWhite = !isWhite;
}
}
} else {
counter.push(0);
for ( i = offset; i < self._row.length; i++) {
if (self._row[i] ^ isWhite) {
counter[counterPos]++;
} else {
counterPos++;
counter.push(0);
counter[counterPos] = 1;
isWhite = !isWhite;
}
}
}
// if cmpCounter was not given
bestMatch.start = offset;
bestMatch.end = self._row.length - 1;
bestMatch.counter = counter;
return bestMatch;
};
BarcodeReader.prototype.decodePattern = function(pattern) {
var self = this,
result;
self._row = pattern;
result = self._decode();
if (result === null) {
self._row.reverse();
result = self._decode();
if (result) {
result.direction = BarcodeReader.DIRECTION.REVERSE;
result.start = self._row.length - result.start;
result.end = self._row.length - result.end;
}
} else {
result.direction = BarcodeReader.DIRECTION.FORWARD;
}
if (result) {
result.format = self.FORMAT;
}
return result;
};
BarcodeReader.prototype._matchRange = function(start, end, value) {
var i;
start = start < 0 ? 0 : start;
for (i = start; i < end; i++) {
if (this._row[i] !== value) {
return false;
}
}
return true;
};
BarcodeReader.prototype._fillCounters = function(offset, end, isWhite) {
var self = this,
counterPos = 0,
i,
counters = [];
isWhite = (typeof isWhite !== 'undefined') ? isWhite : true;
offset = (typeof offset !== 'undefined') ? offset : self._nextUnset(self._row);
end = end || self._row.length;
counters[counterPos] = 0;
for (i = offset; i < end; i++) {
if (self._row[i] ^ isWhite) {
counters[counterPos]++;
} else {
counterPos++;
counters[counterPos] = 1;
isWhite = !isWhite;
}
}
return counters;
};
BarcodeReader.prototype._toCounters = function(start, counter) {
var self = this,
numCounters = counter.length,
end = self._row.length,
isWhite = !self._row[start],
i,
counterPos = 0;
ArrayHelper.init(counter, 0);
for ( i = start; i < end; i++) {
if (self._row[i] ^ isWhite) {
counter[counterPos]++;
} else {
counterPos++;
if (counterPos === numCounters) {
break;
} else {
counter[counterPos] = 1;
isWhite = !isWhite;
}
}
}
return counter;
};
Object.defineProperty(BarcodeReader.prototype, "FORMAT", {
value: 'unknown',
writeable: false
});
BarcodeReader.DIRECTION = {
FORWARD: 1,
REVERSE: -1
};
BarcodeReader.Exception = {
StartNotFoundException: "Start-Info was not found!",
CodeNotFoundException: "Code could not be found!",
PatternNotFoundException: "Pattern could not be found!"
};
BarcodeReader.CONFIG_KEYS = {};
export default BarcodeReader;

@ -0,0 +1,281 @@
import { Barcode, BarcodeInfo, BarcodeReader } from './barcode-reader';
const ALPHABETH_STRING = '0123456789-$:/.+ABCD';
const ALPHABET = [...ALPHABETH_STRING].map(char => char.charCodeAt(0));
// const ALPHABET = [48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 45, 36, 58, 47, 46, 43, 65, 66, 67, 68];
const CHARACTER_ENCODINGS = [0x003, 0x006, 0x009, 0x060, 0x012, 0x042, 0x021, 0x024, 0x030, 0x048, 0x00c, 0x018, 0x045,
0x051, 0x054, 0x015, 0x01A, 0x029, 0x00B, 0x00E];
const START_END = [0x01A, 0x029, 0x00B, 0x00E];
const MIN_ENCODED_CHARS = 4;
const MAX_ACCEPTABLE = 2.0;
const PADDING = 1.5;
interface Threshold {
space: {
narrow: {
size: number;
counts: number;
min: number;
max: number;
};
wide: {
size: number;
counts: number;
min: number;
max: number;
};
};
bar: {
narrow: {
size: number;
counts: number;
min: number;
max: number;
};
wide: {
size: number;
counts: number;
min: number;
max: number;
};
};
}
export class CodabarReader extends BarcodeReader {
private _counters: Array<number>;
constructor() {
super();
this._format = 'codabar';
this._counters = [];
}
decode(): Barcode {
this._counters = this._fillCounters(this._nextUnset(this._row), this._row.length, 1);
const start = this._findStart();
if (!start) {
return null;
}
const result = new Array<string>();
let nextStart = start.startCounter;
let pattern: number;
do {
pattern = this._toPattern(nextStart);
if (pattern < 0) {
return null;
}
const decodedChar = this._patternToChar(pattern);
if (decodedChar === null) {
return null;
}
result.push(decodedChar);
nextStart += 8;
if (result.length > 1 && START_END.some(code => code === pattern)) {
break;
}
} while (nextStart < this._counters.length);
// verify end
if ((result.length - 2) < MIN_ENCODED_CHARS || !START_END.some(code => code === pattern)) {
return null;
}
// verify end white space
if (!this._verifyWhitespace(start.startCounter, nextStart - 8)) {
return null;
}
if (!this._validateResult(result, start.startCounter)) {
return null;
}
nextStart = nextStart > this._counters.length ? this._counters.length : nextStart;
const end = start.start + this._sumCounters(start.startCounter, nextStart - 8);
return {
code: result.join(''),
start: start.start,
end,
startInfo: start,
decodedCodes: result
};
}
protected _verifyWhitespace(startCounter: number, endCounter: number): boolean {
if ((startCounter - 1 <= 0)
|| this._counters[startCounter - 1] >= (this._calculatePatternLength(startCounter) / 2.0)) {
if ((endCounter + 8 >= this._counters.length)
|| this._counters[endCounter + 7] >= (this._calculatePatternLength(endCounter) / 2.0)) {
return true;
}
}
return false;
}
private _calculatePatternLength(offset: number): number {
let sum = 0;
for (let i = offset; i < offset + 7; i++) {
sum += this._counters[i];
}
return sum;
}
private _thresholdResultPattern(result: ReadonlyArray<string>, startCounter: number): Threshold {
const categorization: Threshold = {
space: {
narrow: { size: 0, counts: 0, min: 0, max: Number.MAX_VALUE },
wide: { size: 0, counts: 0, min: 0, max: Number.MAX_VALUE }
},
bar: {
narrow: { size: 0, counts: 0, min: 0, max: Number.MAX_VALUE },
wide: { size: 0, counts: 0, min: 0, max: Number.MAX_VALUE }
}
};
let pos = startCounter;
for (let i = 0; i < result.length; i++) {
let pattern = this._charToPattern(result[i]);
for (let j = 6; j >= 0; j--) {
const kind = (j & 1) === 2 ? categorization.bar : categorization.space;
const cat = (pattern & 1) === 1 ? kind.wide : kind.narrow;
cat.size += this._counters[pos + j];
cat.counts++;
pattern >>= 1;
}
pos += 8;
}
['space', 'bar'].forEach(key => {
const kind = categorization[key];
kind.wide.min = Math.floor((kind.narrow.size / kind.narrow.counts + kind.wide.size / kind.wide.counts) / 2);
kind.narrow.max = Math.ceil(kind.wide.min);
kind.wide.max = Math.ceil((kind.wide.size * MAX_ACCEPTABLE + PADDING) / kind.wide.counts);
});
return categorization;
}
private _charToPattern(char: string): number {
const charCode = char.charCodeAt(0);
for (let i = 0; i < ALPHABET.length; i++) {
if (ALPHABET[i] === charCode) {
return CHARACTER_ENCODINGS[i];
}
}
return 0x0;
}
private _validateResult(result: ReadonlyArray<string>, startCounter: number): boolean {
const threshold = this._thresholdResultPattern(result, startCounter);
let pos = startCounter;
for (let i = 0; i < result.length; i++) {
let pattern = this._charToPattern(result[i]);
for (let j = 6; j >= 0; j--) {
const kind = (j & 1) === 0 ? threshold.bar : threshold.space;
const cat = (pattern & 1) === 1 ? kind.wide : kind.narrow;
const size = this._counters[pos + j];
if (size < cat.min || size > cat.max) {
return false;
}
pattern >>= 1;
}
pos += 8;
}
return true;
}
private _patternToChar(pattern: number): string {
for (let i = 0; i < CHARACTER_ENCODINGS.length; i++) {
if (CHARACTER_ENCODINGS[i] === pattern) {
return String.fromCharCode(ALPHABET[i]);
}
}
return null;
}
private _computeAlternatingThreshold(offset: number, end: number): number {
let min = Number.MAX_VALUE;
let max = 0;
for (let i = offset; i < end; i += 2) {
const counter = this._counters[i];
if (counter > max) {
max = counter;
}
if (counter < min) {
min = counter;
}
}
return ((min + max) / 2.0) | 0;
}
private _toPattern(offset: number): number {
const numCounters = 7;
const end = offset + numCounters;
if (end > this._counters.length) {
return -1;
}
const barThreshold = this._computeAlternatingThreshold(offset, end);
const spaceThreshold = this._computeAlternatingThreshold(offset + 1, end);
let bitmask = 1 << (numCounters - 1);
let pattern = 0;
for (let i = 0; i < numCounters; i++) {
const threshold = (i & 1) === 0 ? barThreshold : spaceThreshold;
if (this._counters[offset + i] > threshold) {
pattern |= bitmask;
}
bitmask >>= 1;
}
return pattern;
}
private _sumCounters(start: number, end: number): number {
let sum = 0;
for (let i = start; i < end; i++) {
sum += this._counters[i];
}
return sum;
}
protected _findStart(): BarcodeInfo {
let start = this._nextUnset(this._row);
for (let i = 1; i < this._counters.length; i++) {
const pattern = this._toPattern(i);
if (pattern !== -1 && START_END.some(code => code === pattern)) {
// TODO: Look for whitespace ahead
start += this._sumCounters(0, i);
const end = start + this._sumCounters(i, i + 8);
return {
start,
end,
startCounter: i,
endCounter: i + 8
};
}
}
return null;
}
}

@ -1,288 +0,0 @@
import BarcodeReader from './barcode_reader';
function CodabarReader() {
BarcodeReader.call(this);
this._counters = [];
}
var properties = {
ALPHABETH_STRING: {value: "0123456789-$:/.+ABCD"},
ALPHABET: {value: [48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 45, 36, 58, 47, 46, 43, 65, 66, 67, 68]},
CHARACTER_ENCODINGS: {value: [0x003, 0x006, 0x009, 0x060, 0x012, 0x042, 0x021, 0x024, 0x030, 0x048, 0x00c, 0x018,
0x045, 0x051, 0x054, 0x015, 0x01A, 0x029, 0x00B, 0x00E]},
START_END: {value: [0x01A, 0x029, 0x00B, 0x00E]},
MIN_ENCODED_CHARS: {value: 4},
MAX_ACCEPTABLE: {value: 2.0},
PADDING: {value: 1.5},
FORMAT: {value: "codabar", writeable: false}
};
CodabarReader.prototype = Object.create(BarcodeReader.prototype, properties);
CodabarReader.prototype.constructor = CodabarReader;
CodabarReader.prototype._decode = function() {
var self = this,
result = [],
start,
decodedChar,
pattern,
nextStart,
end;
this._counters = self._fillCounters();
start = self._findStart();
if (!start) {
return null;
}
nextStart = start.startCounter;
do {
pattern = self._toPattern(nextStart);
if (pattern < 0) {
return null;
}
decodedChar = self._patternToChar(pattern);
if (decodedChar < 0){
return null;
}
result.push(decodedChar);
nextStart += 8;
if (result.length > 1 && self._isStartEnd(pattern)) {
break;
}
} while (nextStart < self._counters.length);
// verify end
if ((result.length - 2) < self.MIN_ENCODED_CHARS || !self._isStartEnd(pattern)) {
return null;
}
// verify end white space
if (!self._verifyWhitespace(start.startCounter, nextStart - 8)){
return null;
}
if (!self._validateResult(result, start.startCounter)){
return null;
}
nextStart = nextStart > self._counters.length ? self._counters.length : nextStart;
end = start.start + self._sumCounters(start.startCounter, nextStart - 8);
return {
code: result.join(""),
start: start.start,
end: end,
startInfo: start,
decodedCodes: result
};
};
CodabarReader.prototype._verifyWhitespace = function(startCounter, endCounter) {
if ((startCounter - 1 <= 0)
|| this._counters[startCounter - 1] >= (this._calculatePatternLength(startCounter) / 2.0)) {
if ((endCounter + 8 >= this._counters.length)
|| this._counters[endCounter + 7] >= (this._calculatePatternLength(endCounter) / 2.0)) {
return true;
}
}
return false;
};
CodabarReader.prototype._calculatePatternLength = function(offset) {
var i,
sum = 0;
for (i = offset; i < offset + 7; i++) {
sum += this._counters[i];
}
return sum;
};
CodabarReader.prototype._thresholdResultPattern = function(result, startCounter){
var self = this,
categorization = {
space: {
narrow: { size: 0, counts: 0, min: 0, max: Number.MAX_VALUE},
wide: {size: 0, counts: 0, min: 0, max: Number.MAX_VALUE}
},
bar: {
narrow: { size: 0, counts: 0, min: 0, max: Number.MAX_VALUE},
wide: { size: 0, counts: 0, min: 0, max: Number.MAX_VALUE}
}
},
kind,
cat,
i,
j,
pos = startCounter,
pattern;
for (i = 0; i < result.length; i++){
pattern = self._charToPattern(result[i]);
for (j = 6; j >= 0; j--) {
kind = (j & 1) === 2 ? categorization.bar : categorization.space;
cat = (pattern & 1) === 1 ? kind.wide : kind.narrow;
cat.size += self._counters[pos + j];
cat.counts++;
pattern >>= 1;
}
pos += 8;
}
["space", "bar"].forEach(function(key) {
var newkind = categorization[key];
newkind.wide.min =
Math.floor((newkind.narrow.size / newkind.narrow.counts + newkind.wide.size / newkind.wide.counts) / 2);
newkind.narrow.max = Math.ceil(newkind.wide.min);
newkind.wide.max = Math.ceil((newkind.wide.size * self.MAX_ACCEPTABLE + self.PADDING) / newkind.wide.counts);
});
return categorization;
};
CodabarReader.prototype._charToPattern = function(char) {
var self = this,
charCode = char.charCodeAt(0),
i;
for (i = 0; i < self.ALPHABET.length; i++) {
if (self.ALPHABET[i] === charCode){
return self.CHARACTER_ENCODINGS[i];
}
}
return 0x0;
};
CodabarReader.prototype._validateResult = function(result, startCounter) {
var self = this,
thresholds = self._thresholdResultPattern(result, startCounter),
i,
j,
kind,
cat,
size,
pos = startCounter,
pattern;
for (i = 0; i < result.length; i++) {
pattern = self._charToPattern(result[i]);
for (j = 6; j >= 0; j--) {
kind = (j & 1) === 0 ? thresholds.bar : thresholds.space;
cat = (pattern & 1) === 1 ? kind.wide : kind.narrow;
size = self._counters[pos + j];
if (size < cat.min || size > cat.max) {
return false;
}
pattern >>= 1;
}
pos += 8;
}
return true;
};
CodabarReader.prototype._patternToChar = function(pattern) {
var i,
self = this;
for (i = 0; i < self.CHARACTER_ENCODINGS.length; i++) {
if (self.CHARACTER_ENCODINGS[i] === pattern) {
return String.fromCharCode(self.ALPHABET[i]);
}
}
return -1;
};
CodabarReader.prototype._computeAlternatingThreshold = function(offset, end) {
var i,
min = Number.MAX_VALUE,
max = 0,
counter;
for (i = offset; i < end; i += 2){
counter = this._counters[i];
if (counter > max) {
max = counter;
}
if (counter < min) {
min = counter;
}
}
return ((min + max) / 2.0) | 0;
};
CodabarReader.prototype._toPattern = function(offset) {
var numCounters = 7,
end = offset + numCounters,
barThreshold,
spaceThreshold,
bitmask = 1 << (numCounters - 1),
pattern = 0,
i,
threshold;
if (end > this._counters.length) {
return -1;
}
barThreshold = this._computeAlternatingThreshold(offset, end);
spaceThreshold = this._computeAlternatingThreshold(offset + 1, end);
for (i = 0; i < numCounters; i++){
threshold = (i & 1) === 0 ? barThreshold : spaceThreshold;
if (this._counters[offset + i] > threshold) {
pattern |= bitmask;
}
bitmask >>= 1;
}
return pattern;
};
CodabarReader.prototype._isStartEnd = function(pattern) {
var i;
for (i = 0; i < this.START_END.length; i++) {
if (this.START_END[i] === pattern) {
return true;
}
}
return false;
};
CodabarReader.prototype._sumCounters = function(start, end) {
var i,
sum = 0;
for (i = start; i < end; i++) {
sum += this._counters[i];
}
return sum;
};
CodabarReader.prototype._findStart = function() {
var self = this,
i,
pattern,
start = self._nextUnset(self._row),
end;
for (i = 1; i < this._counters.length; i++) {
pattern = self._toPattern(i);
if (pattern !== -1 && self._isStartEnd(pattern)) {
// TODO: Look for whitespace ahead
start += self._sumCounters(0, i);
end = start + self._sumCounters(i, i + 8);
return {
start: start,
end: end,
startCounter: i,
endCounter: i + 8
};
}
}
};
export default CodabarReader;

@ -0,0 +1,459 @@
import { Barcode, BarcodeCorrection, BarcodeInfo, BarcodeReader } from './barcode-reader';
const CODE_SHIFT = 98;
const CODE_C = 99;
const CODE_B = 100;
const CODE_A = 101;
const START_CODE_A = 103;
const START_CODE_B = 104;
const START_CODE_C = 105;
const STOP_CODE = 106;
const CODE_PATTERN = [
[2, 1, 2, 2, 2, 2],
[2, 2, 2, 1, 2, 2],
[2, 2, 2, 2, 2, 1],
[1, 2, 1, 2, 2, 3],
[1, 2, 1, 3, 2, 2],
[1, 3, 1, 2, 2, 2],
[1, 2, 2, 2, 1, 3],
[1, 2, 2, 3, 1, 2],
[1, 3, 2, 2, 1, 2],
[2, 2, 1, 2, 1, 3],
[2, 2, 1, 3, 1, 2],
[2, 3, 1, 2, 1, 2],
[1, 1, 2, 2, 3, 2],
[1, 2, 2, 1, 3, 2],
[1, 2, 2, 2, 3, 1],
[1, 1, 3, 2, 2, 2],
[1, 2, 3, 1, 2, 2],
[1, 2, 3, 2, 2, 1],
[2, 2, 3, 2, 1, 1],
[2, 2, 1, 1, 3, 2],
[2, 2, 1, 2, 3, 1],
[2, 1, 3, 2, 1, 2],
[2, 2, 3, 1, 1, 2],
[3, 1, 2, 1, 3, 1],
[3, 1, 1, 2, 2, 2],
[3, 2, 1, 1, 2, 2],
[3, 2, 1, 2, 2, 1],
[3, 1, 2, 2, 1, 2],
[3, 2, 2, 1, 1, 2],
[3, 2, 2, 2, 1, 1],
[2, 1, 2, 1, 2, 3],
[2, 1, 2, 3, 2, 1],
[2, 3, 2, 1, 2, 1],
[1, 1, 1, 3, 2, 3],
[1, 3, 1, 1, 2, 3],
[1, 3, 1, 3, 2, 1],
[1, 1, 2, 3, 1, 3],
[1, 3, 2, 1, 1, 3],
[1, 3, 2, 3, 1, 1],
[2, 1, 1, 3, 1, 3],
[2, 3, 1, 1, 1, 3],
[2, 3, 1, 3, 1, 1],
[1, 1, 2, 1, 3, 3],
[1, 1, 2, 3, 3, 1],
[1, 3, 2, 1, 3, 1],
[1, 1, 3, 1, 2, 3],
[1, 1, 3, 3, 2, 1],
[1, 3, 3, 1, 2, 1],
[3, 1, 3, 1, 2, 1],
[2, 1, 1, 3, 3, 1],
[2, 3, 1, 1, 3, 1],
[2, 1, 3, 1, 1, 3],
[2, 1, 3, 3, 1, 1],
[2, 1, 3, 1, 3, 1],
[3, 1, 1, 1, 2, 3],
[3, 1, 1, 3, 2, 1],
[3, 3, 1, 1, 2, 1],
[3, 1, 2, 1, 1, 3],
[3, 1, 2, 3, 1, 1],
[3, 3, 2, 1, 1, 1],
[3, 1, 4, 1, 1, 1],
[2, 2, 1, 4, 1, 1],
[4, 3, 1, 1, 1, 1],
[1, 1, 1, 2, 2, 4],
[1, 1, 1, 4, 2, 2],
[1, 2, 1, 1, 2, 4],
[1, 2, 1, 4, 2, 1],
[1, 4, 1, 1, 2, 2],
[1, 4, 1, 2, 2, 1],
[1, 1, 2, 2, 1, 4],
[1, 1, 2, 4, 1, 2],
[1, 2, 2, 1, 1, 4],
[1, 2, 2, 4, 1, 1],
[1, 4, 2, 1, 1, 2],
[1, 4, 2, 2, 1, 1],
[2, 4, 1, 2, 1, 1],
[2, 2, 1, 1, 1, 4],
[4, 1, 3, 1, 1, 1],
[2, 4, 1, 1, 1, 2],
[1, 3, 4, 1, 1, 1],
[1, 1, 1, 2, 4, 2],
[1, 2, 1, 1, 4, 2],
[1, 2, 1, 2, 4, 1],
[1, 1, 4, 2, 1, 2],
[1, 2, 4, 1, 1, 2],
[1, 2, 4, 2, 1, 1],
[4, 1, 1, 2, 1, 2],
[4, 2, 1, 1, 1, 2],
[4, 2, 1, 2, 1, 1],
[2, 1, 2, 1, 4, 1],
[2, 1, 4, 1, 2, 1],
[4, 1, 2, 1, 2, 1],
[1, 1, 1, 1, 4, 3],
[1, 1, 1, 3, 4, 1],
[1, 3, 1, 1, 4, 1],
[1, 1, 4, 1, 1, 3],
[1, 1, 4, 3, 1, 1],
[4, 1, 1, 1, 1, 3],
[4, 1, 1, 3, 1, 1],
[1, 1, 3, 1, 4, 1],
[1, 1, 4, 1, 3, 1],
[3, 1, 1, 1, 4, 1],
[4, 1, 1, 1, 3, 1],
[2, 1, 1, 4, 1, 2],
[2, 1, 1, 2, 1, 4],
[2, 1, 1, 2, 3, 2],
[2, 3, 3, 1, 1, 1, 2]
];
const MODULE_INDICES = { bar: [0, 2, 4], space: [1, 3, 5] };
export class Code128Reader extends BarcodeReader {
constructor() {
super();
this._format = 'code_128';
this._singleCodeError = 0.64;
this._averageCodeError = 0.30;
}
protected _decodeCode(start: number, correction: BarcodeCorrection): BarcodeInfo {
const counter = [0, 0, 0, 0, 0, 0];
const offset = start;
const bestMatch: BarcodeInfo = {
error: Number.MAX_VALUE,
code: -1,
start: start,
end: start,
correction: {
bar: 1,
space: 1
}
};
const epsilon = this.AVERAGE_CODE_ERROR;
let isWhite: 0 | 1 = this._row[offset] ? 0 : 1;
let counterPos = 0;
for (let i = offset; i < this._row.length; i++) {
if (this._row[i] ^ isWhite) {
counter[counterPos]++;
} else {
if (counterPos === counter.length - 1) {
if (correction) {
this._correct(counter, correction);
}
for (let code = 0; code < CODE_PATTERN.length; code++) {
const error = this._matchPattern(counter, CODE_PATTERN[code]);
if (error < bestMatch.error) {
bestMatch.code = code;
bestMatch.error = error;
}
}
bestMatch.end = i;
if (bestMatch.code === -1 || bestMatch.error > epsilon) {
return null;
}
const expected = CODE_PATTERN[bestMatch.code];
if (expected) {
bestMatch.correction.bar = this._calculateCorrection(expected, counter, MODULE_INDICES.bar);
bestMatch.correction.space = this._calculateCorrection(expected, counter, MODULE_INDICES.space);
}
return bestMatch;
} else {
counterPos++;
}
counter[counterPos] = 1;
isWhite = isWhite ? 0 : 1;
}
}
return null;
}
private _correct(counter: Array<number>, correction: BarcodeCorrection): void {
this._correctBars(counter, correction.bar, MODULE_INDICES.bar);
this._correctBars(counter, correction.space, MODULE_INDICES.space);
}
protected _findStart() {
const counter = [0, 0, 0, 0, 0, 0];
const offset = this._nextSet(this._row);
const bestMatch = {
error: Number.MAX_VALUE,
code: -1,
start: 0,
end: 0,
correction: {
bar: 1,
space: 1
}
};
const epsilon = this.AVERAGE_CODE_ERROR;
let isWhite: 0 | 1 = 0;
let counterPos = 0;
let sum: number;
for (let i = offset; i < this._row.length; i++) {
if (this._row[i] ^ isWhite) {
counter[counterPos]++;
} else {
if (counterPos === counter.length - 1) {
sum = 0;
for (let j = 0; j < counter.length; j++) {
sum += counter[j];
}
for (let code = START_CODE_A; code <= START_CODE_C; code++) {
const error = this._matchPattern(counter, CODE_PATTERN[code]);
if (error < bestMatch.error) {
bestMatch.code = code;
bestMatch.error = error;
}
}
if (bestMatch.error < epsilon) {
bestMatch.start = i - sum;
bestMatch.end = i;
bestMatch.correction.bar = this._calculateCorrection(CODE_PATTERN[bestMatch.code], counter,
MODULE_INDICES.bar);
bestMatch.correction.space = this._calculateCorrection(CODE_PATTERN[bestMatch.code], counter,
MODULE_INDICES.space);
return bestMatch;
}
for (let j = 0; j < 4; j++) {
counter[j] = counter[j + 2];
}
counter[4] = 0;
counter[5] = 0;
counterPos--;
} else {
counterPos++;
}
counter[counterPos] = 1;
isWhite = isWhite ? 0 : 1;
}
}
return null;
}
decode(): Barcode {
const result = new Array<string | number>();
const startInfo = this._findStart();
let code: BarcodeInfo = null;
let done = false;
let multiplier = 0;
let checksum = 0;
let codeset: number;
let rawResult = new Array<number>();
let decodedCodes = new Array<BarcodeInfo>();
let shiftNext = false;
let unshift: boolean;
let removeLastCharacter = true;
if (startInfo === null) {
return null;
}
code = {
code: startInfo.code,
start: startInfo.start,
end: startInfo.end,
correction: {
bar: startInfo.correction.bar,
space: startInfo.correction.space
}
};
decodedCodes.push(code);
checksum = code.code;
switch (code.code) {
case START_CODE_A:
codeset = CODE_A;
break;
case START_CODE_B:
codeset = CODE_B;
break;
case START_CODE_C:
codeset = CODE_C;
break;
default:
return null;
}
while (!done) {
unshift = shiftNext;
shiftNext = false;
code = this._decodeCode(code.end, code.correction);
if (code !== null) {
if (code.code !== STOP_CODE) {
removeLastCharacter = true;
}
if (code.code !== STOP_CODE) {
rawResult.push(code.code);
multiplier++;
checksum += multiplier * code.code;
}
decodedCodes.push(code);
switch (codeset) {
case CODE_A: {
if (code.code < 64) {
result.push(String.fromCharCode(32 + code.code));
} else if (code.code < 96) {
result.push(String.fromCharCode(code.code - 64));
} else {
if (code.code !== STOP_CODE) {
removeLastCharacter = false;
}
switch (code.code) {
case CODE_SHIFT:
shiftNext = true;
codeset = CODE_B;
break;
case CODE_B:
codeset = CODE_B;
break;
case CODE_C:
codeset = CODE_C;
break;
case STOP_CODE:
done = true;
break;
}
}
break;
}
case CODE_B: {
if (code.code < 96) {
result.push(String.fromCharCode(32 + code.code));
} else {
if (code.code !== STOP_CODE) {
removeLastCharacter = false;
}
switch (code.code) {
case CODE_SHIFT:
shiftNext = true;
codeset = CODE_A;
break;
case CODE_A:
codeset = CODE_A;
break;
case CODE_C:
codeset = CODE_C;
break;
case STOP_CODE:
done = true;
break;
}
}
break;
}
case CODE_C: {
if (code.code < 100) {
result.push(code.code < 10 ? '0' + code.code : code.code);
} else {
if (code.code !== STOP_CODE) {
removeLastCharacter = false;
}
switch (code.code) {
case CODE_A:
codeset = CODE_A;
break;
case CODE_B:
codeset = CODE_B;
break;
case STOP_CODE:
done = true;
break;
}
}
break;
}
}
} else {
done = true;
}
if (unshift) {
codeset = codeset === CODE_A ? CODE_B : CODE_A;
}
}
if (code === null) {
return null;
}
code.end = this._nextUnset(this._row, code.end);
if (!this._verifyTrailingWhitespace(code)) {
return null;
}
checksum -= multiplier * rawResult[rawResult.length - 1];
if (checksum % 103 !== rawResult[rawResult.length - 1]) {
return null;
}
if (!result.length) {
return null;
}
// remove last code from result (checksum)
if (removeLastCharacter) {
result.splice(result.length - 1, 1);
}
return {
code: result.join(''),
start: startInfo.start,
end: code.end,
codeset,
startInfo,
decodedCodes,
endInfo: code
};
}
protected _verifyTrailingWhitespace(endInfo: BarcodeInfo): BarcodeInfo {
const trailingWhitespaceEnd = endInfo.end + (endInfo.end - endInfo.start) / 2;
if (trailingWhitespaceEnd < this._row.length) {
if (this._matchRange(endInfo.end, trailingWhitespaceEnd, 0)) {
return endInfo;
}
}
return null;
}
private _calculateCorrection(
expected: ReadonlyArray<number>,
normalized: ReadonlyArray<number>,
indices: ReadonlyArray<number>
): number {
let sumNormalized = 0;
let sumExpected = 0;
for (let length = indices.length; length--;) {
sumExpected += expected[indices[length]];
sumNormalized += normalized[indices[length]];
}
return sumExpected / sumNormalized;
}
}

@ -0,0 +1,170 @@
import { Barcode, BarcodeInfo, BarcodeReader } from './barcode-reader';
const ASTERISK = 0x094;
const ALPHABETH_STRING = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. *$/+%';
const ALPHABET = new Uint16Array([...ALPHABETH_STRING].map(char => char.charCodeAt(0)));
// const ALPHABET = [48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78,
// 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 45, 46, 32, 42, 36, 47, 43, 37];
const CHARACTER_ENCODINGS = new Uint16Array([
0x034, 0x121, 0x061, 0x160, 0x031, 0x130, 0x070, 0x025, 0x124, 0x064, 0x109, 0x049, 0x148, 0x019, 0x118, 0x058,
0x00D, 0x10C, 0x04C, 0x01C, 0x103, 0x043, 0x142, 0x013, 0x112, 0x052, 0x007, 0x106, 0x046, 0x016, 0x181, 0x0C1,
0x1C0, 0x091, 0x190, 0x0D0, 0x085, 0x184, 0x0C4, 0x094, 0x0A8, 0x0A2, 0x08A, 0x02A
]);
export class Code39Reader extends BarcodeReader {
constructor() {
super();
this._format = 'code_39';
}
decode(): Barcode {
const start = this._findStart();
if (!start) {
return null;
}
const result = new Array<string>();
let counters = new Uint16Array(9);
let decodedChar: string;
let lastStart: number;
let nextStart = this._nextSet(this._row, start.end);
do {
this._toCounters(nextStart, counters);
const pattern = this._toPattern(counters);
if (pattern < 0) {
return null;
}
decodedChar = this._patternToChar(pattern);
if (decodedChar === null) {
return null;
}
result.push(decodedChar);
lastStart = nextStart;
nextStart += counters.reduce((sum, item) => sum + item, 0);
nextStart = this._nextSet(this._row, nextStart);
} while (decodedChar !== '*');
result.pop();
if (!result.length) {
return null;
}
if (!this._verifyTrailingWhitespace(lastStart, nextStart, counters)) {
return null;
}
return {
code: result.join(''),
start: start.start,
end: nextStart,
startInfo: start,
decodedCodes: result
};
}
protected _patternToChar(pattern): string {
for (let i = 0; i < CHARACTER_ENCODINGS.length; i++) {
if (CHARACTER_ENCODINGS[i] === pattern) {
return String.fromCharCode(ALPHABET[i]);
}
}
return null;
}
private _verifyTrailingWhitespace(lastStart: number, nextStart: number, counters: Uint16Array): boolean {
const patternSize = counters.reduce((sum, item) => sum + item, 0);
const trailingWhitespaceEnd = nextStart - lastStart - patternSize;
return (trailingWhitespaceEnd * 3) >= patternSize;
}
private _findNextWidth(counters: Uint16Array, current: number): number {
let minWidth = Number.MAX_VALUE;
for (let i = 0; i < counters.length; i++) {
if (counters[i] < minWidth && counters[i] > current) {
minWidth = counters[i];
}
}
return minWidth;
}
private _toPattern(counters: Uint16Array): number {
const numCounters = counters.length;
let maxNarrowWidth = 0;
let numWideBars = numCounters;
let wideBarWidth = 0;
let pattern: number;
while (numWideBars > 3) {
maxNarrowWidth = this._findNextWidth(counters, maxNarrowWidth);
numWideBars = 0;
pattern = 0;
for (let i = 0; i < numCounters; i++) {
if (counters[i] > maxNarrowWidth) {
pattern |= 1 << (numCounters - 1 - i);
numWideBars++;
wideBarWidth += counters[i];
}
}
if (numWideBars === 3) {
for (let i = 0; i < numCounters && numWideBars > 0; i++) {
if (counters[i] > maxNarrowWidth) {
numWideBars--;
if ((counters[i] * 2) >= wideBarWidth) {
return -1;
}
}
}
return pattern;
}
}
return -1;
}
protected _findStart(): BarcodeInfo {
const offset = this._nextSet(this._row);
let patternStart = offset;
const counter = new Uint16Array(9);
let counterPos = 0;
let isWhite: 0 | 1 = 0;
let whiteSpaceMustStart: number;
for (let i = offset; i < this._row.length; i++) {
if (this._row[i] ^ isWhite) {
counter[counterPos]++;
} else {
if (counterPos === counter.length - 1) {
// find start pattern
if (this._toPattern(counter) === ASTERISK) {
whiteSpaceMustStart = Math.max(0, patternStart - ((i - patternStart) / 4)) | 0;
if (this._matchRange(whiteSpaceMustStart, patternStart, 0)) {
return {
start: patternStart,
end: i
};
}
}
patternStart += counter[0] + counter[1];
for (let j = 0; j < 7; j++) {
counter[j] = counter[j + 2];
}
counter[7] = 0;
counter[8] = 0;
counterPos--;
} else {
counterPos++;
}
counter[counterPos] = 1;
isWhite = isWhite ? 0 : 1;
}
}
return null;
}
}

@ -0,0 +1,48 @@
import { Barcode } from './barcode-reader';
import { Code39Reader } from './code-39-reader';
export class Code39VINReader extends Code39Reader {
constructor() {
super();
this._format = 'code_39_vin';
}
/**
* @borrows
* https://github.com/zxing/zxing/blob/master/core/src/main/java/com/google/zxing/client/result/VINResultParser.java
*/
decode(): Barcode {
const result = super.decode();
if (!result) {
return null;
}
let code = result.code;
if (!code) {
return null;
}
code = code.replace(/[IOQ]/g, '');
if (!/[A-Z0-9]{17}/.test(code)) {
if (process.env.NODE_ENV !== 'production') {
console.log('Failed AZ09 pattern code:', code);
}
return null;
}
if (!this._checkChecksum(code)) {
return null;
}
result.code = code;
return result;
}
private _checkChecksum(code: string): boolean {
// TODO
return !!code;
}
}

@ -0,0 +1,239 @@
import { Barcode, BarcodeInfo, BarcodeReader } from './barcode-reader';
const ALPHABETH_STRING = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%abcd*';
const ALPHABET = new Uint16Array([...ALPHABETH_STRING].map(char => char.charCodeAt(0)));
const CHARACTER_ENCODINGS = new Uint16Array([
0x114, 0x148, 0x144, 0x142, 0x128, 0x124, 0x122, 0x150, 0x112, 0x10A, 0x1A8, 0x1A4, 0x1A2, 0x194, 0x192, 0x18A,
0x168, 0x164, 0x162, 0x134, 0x11A, 0x158, 0x14C, 0x146, 0x12C, 0x116, 0x1B4, 0x1B2, 0x1AC, 0x1A6, 0x196, 0x19A,
0x16C, 0x166, 0x136, 0x13A, 0x12E, 0x1D4, 0x1D2, 0x1CA, 0x16E, 0x176, 0x1AE, 0x126, 0x1DA, 0x1D6, 0x132, 0x15E
]);
const ASTERISK = 0x15E;
export class Code93Reader extends BarcodeReader {
constructor() {
super();
this._format = 'code_93';
}
decode(): Barcode {
const start = this._findStart();
if (!start) {
return null;
}
let result = new Array<string>();
let counters = new Uint16Array(6);
let decodedChar: string;
let lastStart: number;
let nextStart = this._nextSet(this._row, start.end);
do {
this._toCounters(nextStart, counters);
const pattern = this._toPattern(counters);
if (pattern < 0) {
return null;
}
decodedChar = this._patternToChar(pattern);
if (decodedChar === null) {
return null;
}
result.push(decodedChar);
lastStart = nextStart;
nextStart += counters.reduce((sum, item) => sum + item, 0);
nextStart = this._nextSet(this._row, nextStart);
} while (decodedChar !== '*');
result.pop();
if (!result.length) {
return null;
}
if (!this._verifyEnd(lastStart, nextStart)) {
return null;
}
if (!this._verifyChecksums(result)) {
return null;
}
result = result.slice(0, result.length - 2);
if ((result = this._decodeExtended(result)) === null) {
return null;
}
return {
code: result.join(''),
start: start.start,
end: nextStart,
startInfo: start,
decodedCodes: result
};
}
protected _patternToChar(pattern: number): string {
for (let i = 0; i < CHARACTER_ENCODINGS.length; i++) {
if (CHARACTER_ENCODINGS[i] === pattern) {
return String.fromCharCode(ALPHABET[i]);
}
}
return null;
}
private _verifyEnd(lastStart: number, nextStart: number): boolean {
if (lastStart === nextStart || !this._row[nextStart]) {
return false;
}
return true;
}
private _toPattern(counters: Uint16Array): number {
const numCounters = counters.length;
let pattern = 0;
let sum = 0;
for (let i = 0; i < numCounters; i++) {
sum += counters[i];
}
for (let i = 0; i < numCounters; i++) {
let normalized = Math.round(counters[i] * 9 / sum);
if (normalized < 1 || normalized > 4) {
return -1;
}
if ((i & 1) === 0) {
for (let j = 0; j < normalized; j++) {
pattern = (pattern << 1) | 1;
}
} else {
pattern <<= normalized;
}
}
return pattern;
}
private _findStart(): BarcodeInfo {
const counter = new Uint16Array(6);
const offset = this._nextSet(this._row);
let patternStart = offset;
let counterPos = 0;
let isWhite: 0 | 1 = 0;
let whiteSpaceMustStart: number;
for (let i = offset; i < this._row.length; i++) {
if (this._row[i] ^ isWhite) {
counter[counterPos]++;
} else {
if (counterPos === counter.length - 1) {
// find start pattern
if (this._toPattern(counter) === ASTERISK) {
whiteSpaceMustStart = Math.max(0, patternStart - ((i - patternStart) / 4)) | 0;
if (this._matchRange(whiteSpaceMustStart, patternStart, 0)) {
return {
start: patternStart,
end: i
};
}
}
patternStart += counter[0] + counter[1];
for (let j = 0; j < 4; j++) {
counter[j] = counter[j + 2];
}
counter[4] = 0;
counter[5] = 0;
counterPos--;
} else {
counterPos++;
}
counter[counterPos] = 1;
isWhite = isWhite ? 0 : 1;
}
}
return null;
}
private _decodeExtended(charArray: Array<string>): Array<string> {
const length = charArray.length;
const result = new Array<string>();
for (let i = 0; i < length; i++) {
const char = charArray[i];
if (char >= 'a' && char <= 'd') {
if (i > (length - 2)) {
return null;
}
const nextChar = charArray[++i];
const nextCharCode = nextChar.charCodeAt(0);
let decodedChar: string;
switch (char) {
case 'a': {
if (nextChar >= 'A' && nextChar <= 'Z') {
decodedChar = String.fromCharCode(nextCharCode - 64);
} else {
return null;
}
break;
}
case 'b': {
if (nextChar >= 'A' && nextChar <= 'E') {
decodedChar = String.fromCharCode(nextCharCode - 38);
} else if (nextChar >= 'F' && nextChar <= 'J') {
decodedChar = String.fromCharCode(nextCharCode - 11);
} else if (nextChar >= 'K' && nextChar <= 'O') {
decodedChar = String.fromCharCode(nextCharCode + 16);
} else if (nextChar >= 'P' && nextChar <= 'S') {
decodedChar = String.fromCharCode(nextCharCode + 43);
} else if (nextChar >= 'T' && nextChar <= 'Z') {
decodedChar = String.fromCharCode(127);
} else {
return null;
}
break;
}
case 'c': {
if (nextChar >= 'A' && nextChar <= 'O') {
decodedChar = String.fromCharCode(nextCharCode - 32);
} else if (nextChar === 'Z') {
decodedChar = ':';
} else {
return null;
}
break;
}
case 'd': {
if (nextChar >= 'A' && nextChar <= 'Z') {
decodedChar = String.fromCharCode(nextCharCode + 32);
} else {
return null;
}
break;
}
}
result.push(decodedChar);
} else {
result.push(char);
}
}
return result;
}
private _verifyChecksums(charArray: Array<string>): boolean {
return this._matchCheckChar(charArray, charArray.length - 2, 20)
&& this._matchCheckChar(charArray, charArray.length - 1, 15);
}
private _matchCheckChar(charArray: Array<string>, index: number, maxWeight: number): boolean {
const arrayToCheck = charArray.slice(0, index);
const length = arrayToCheck.length;
const weightedSums = arrayToCheck.reduce((sum, char, i) => {
const weight = (((i * -1) + (length - 1)) % maxWeight) + 1;
const value = ALPHABET.indexOf(char.charCodeAt(0));
return sum + (weight * value);
}, 0);
const checkChar = ALPHABET[(weightedSums % 47)];
return checkChar === charArray[index].charCodeAt(0);
}
}

@ -1,463 +0,0 @@
import BarcodeReader from './barcode_reader';
function Code128Reader() {
BarcodeReader.call(this);
}
var properties = {
CODE_SHIFT: {value: 98},
CODE_C: {value: 99},
CODE_B: {value: 100},
CODE_A: {value: 101},
START_CODE_A: {value: 103},
START_CODE_B: {value: 104},
START_CODE_C: {value: 105},
STOP_CODE: {value: 106},
CODE_PATTERN: {value: [
[2, 1, 2, 2, 2, 2],
[2, 2, 2, 1, 2, 2],
[2, 2, 2, 2, 2, 1],
[1, 2, 1, 2, 2, 3],
[1, 2, 1, 3, 2, 2],
[1, 3, 1, 2, 2, 2],
[1, 2, 2, 2, 1, 3],
[1, 2, 2, 3, 1, 2],
[1, 3, 2, 2, 1, 2],
[2, 2, 1, 2, 1, 3],
[2, 2, 1, 3, 1, 2],
[2, 3, 1, 2, 1, 2],
[1, 1, 2, 2, 3, 2],
[1, 2, 2, 1, 3, 2],
[1, 2, 2, 2, 3, 1],
[1, 1, 3, 2, 2, 2],
[1, 2, 3, 1, 2, 2],
[1, 2, 3, 2, 2, 1],
[2, 2, 3, 2, 1, 1],
[2, 2, 1, 1, 3, 2],
[2, 2, 1, 2, 3, 1],
[2, 1, 3, 2, 1, 2],
[2, 2, 3, 1, 1, 2],
[3, 1, 2, 1, 3, 1],
[3, 1, 1, 2, 2, 2],
[3, 2, 1, 1, 2, 2],
[3, 2, 1, 2, 2, 1],
[3, 1, 2, 2, 1, 2],
[3, 2, 2, 1, 1, 2],
[3, 2, 2, 2, 1, 1],
[2, 1, 2, 1, 2, 3],
[2, 1, 2, 3, 2, 1],
[2, 3, 2, 1, 2, 1],
[1, 1, 1, 3, 2, 3],
[1, 3, 1, 1, 2, 3],
[1, 3, 1, 3, 2, 1],
[1, 1, 2, 3, 1, 3],
[1, 3, 2, 1, 1, 3],
[1, 3, 2, 3, 1, 1],
[2, 1, 1, 3, 1, 3],
[2, 3, 1, 1, 1, 3],
[2, 3, 1, 3, 1, 1],
[1, 1, 2, 1, 3, 3],
[1, 1, 2, 3, 3, 1],
[1, 3, 2, 1, 3, 1],
[1, 1, 3, 1, 2, 3],
[1, 1, 3, 3, 2, 1],
[1, 3, 3, 1, 2, 1],
[3, 1, 3, 1, 2, 1],
[2, 1, 1, 3, 3, 1],
[2, 3, 1, 1, 3, 1],
[2, 1, 3, 1, 1, 3],
[2, 1, 3, 3, 1, 1],
[2, 1, 3, 1, 3, 1],
[3, 1, 1, 1, 2, 3],
[3, 1, 1, 3, 2, 1],
[3, 3, 1, 1, 2, 1],
[3, 1, 2, 1, 1, 3],
[3, 1, 2, 3, 1, 1],
[3, 3, 2, 1, 1, 1],
[3, 1, 4, 1, 1, 1],
[2, 2, 1, 4, 1, 1],
[4, 3, 1, 1, 1, 1],
[1, 1, 1, 2, 2, 4],
[1, 1, 1, 4, 2, 2],
[1, 2, 1, 1, 2, 4],
[1, 2, 1, 4, 2, 1],
[1, 4, 1, 1, 2, 2],
[1, 4, 1, 2, 2, 1],
[1, 1, 2, 2, 1, 4],
[1, 1, 2, 4, 1, 2],
[1, 2, 2, 1, 1, 4],
[1, 2, 2, 4, 1, 1],
[1, 4, 2, 1, 1, 2],
[1, 4, 2, 2, 1, 1],
[2, 4, 1, 2, 1, 1],
[2, 2, 1, 1, 1, 4],
[4, 1, 3, 1, 1, 1],
[2, 4, 1, 1, 1, 2],
[1, 3, 4, 1, 1, 1],
[1, 1, 1, 2, 4, 2],
[1, 2, 1, 1, 4, 2],
[1, 2, 1, 2, 4, 1],
[1, 1, 4, 2, 1, 2],
[1, 2, 4, 1, 1, 2],
[1, 2, 4, 2, 1, 1],
[4, 1, 1, 2, 1, 2],
[4, 2, 1, 1, 1, 2],
[4, 2, 1, 2, 1, 1],
[2, 1, 2, 1, 4, 1],
[2, 1, 4, 1, 2, 1],
[4, 1, 2, 1, 2, 1],
[1, 1, 1, 1, 4, 3],
[1, 1, 1, 3, 4, 1],
[1, 3, 1, 1, 4, 1],
[1, 1, 4, 1, 1, 3],
[1, 1, 4, 3, 1, 1],
[4, 1, 1, 1, 1, 3],
[4, 1, 1, 3, 1, 1],
[1, 1, 3, 1, 4, 1],
[1, 1, 4, 1, 3, 1],
[3, 1, 1, 1, 4, 1],
[4, 1, 1, 1, 3, 1],
[2, 1, 1, 4, 1, 2],
[2, 1, 1, 2, 1, 4],
[2, 1, 1, 2, 3, 2],
[2, 3, 3, 1, 1, 1, 2]
]},
SINGLE_CODE_ERROR: {value: 0.64},
AVG_CODE_ERROR: {value: 0.30},
FORMAT: {value: "code_128", writeable: false},
MODULE_INDICES: {value: {bar: [0, 2, 4], space: [1, 3, 5]}}
};
Code128Reader.prototype = Object.create(BarcodeReader.prototype, properties);
Code128Reader.prototype.constructor = Code128Reader;
Code128Reader.prototype._decodeCode = function(start, correction) {
var counter = [0, 0, 0, 0, 0, 0],
i,
self = this,
offset = start,
isWhite = !self._row[offset],
counterPos = 0,
bestMatch = {
error: Number.MAX_VALUE,
code: -1,
start: start,
end: start,
correction: {
bar: 1,
space: 1
}
},
code,
error;
for ( i = offset; i < self._row.length; i++) {
if (self._row[i] ^ isWhite) {
counter[counterPos]++;
} else {
if (counterPos === counter.length - 1) {
if (correction) {
self._correct(counter, correction);
}
for (code = 0; code < self.CODE_PATTERN.length; code++) {
error = self._matchPattern(counter, self.CODE_PATTERN[code]);
if (error < bestMatch.error) {
bestMatch.code = code;
bestMatch.error = error;
}
}
bestMatch.end = i;
if (bestMatch.code === -1 || bestMatch.error > self.AVG_CODE_ERROR) {
return null;
}
if (self.CODE_PATTERN[bestMatch.code]) {
bestMatch.correction.bar = calculateCorrection(
self.CODE_PATTERN[bestMatch.code], counter,
this.MODULE_INDICES.bar);
bestMatch.correction.space = calculateCorrection(
self.CODE_PATTERN[bestMatch.code], counter,
this.MODULE_INDICES.space);
}
return bestMatch;
} else {
counterPos++;
}
counter[counterPos] = 1;
isWhite = !isWhite;
}
}
return null;
};
Code128Reader.prototype._correct = function(counter, correction) {
this._correctBars(counter, correction.bar, this.MODULE_INDICES.bar);
this._correctBars(counter, correction.space, this.MODULE_INDICES.space);
};
Code128Reader.prototype._findStart = function() {
var counter = [0, 0, 0, 0, 0, 0],
i,
self = this,
offset = self._nextSet(self._row),
isWhite = false,
counterPos = 0,
bestMatch = {
error: Number.MAX_VALUE,
code: -1,
start: 0,
end: 0,
correction: {
bar: 1,
space: 1
}
},
code,
error,
j,
sum;
for ( i = offset; i < self._row.length; i++) {
if (self._row[i] ^ isWhite) {
counter[counterPos]++;
} else {
if (counterPos === counter.length - 1) {
sum = 0;
for ( j = 0; j < counter.length; j++) {
sum += counter[j];
}
for (code = self.START_CODE_A; code <= self.START_CODE_C; code++) {
error = self._matchPattern(counter, self.CODE_PATTERN[code]);
if (error < bestMatch.error) {
bestMatch.code = code;
bestMatch.error = error;
}
}
if (bestMatch.error < self.AVG_CODE_ERROR) {
bestMatch.start = i - sum;
bestMatch.end = i;
bestMatch.correction.bar = calculateCorrection(
self.CODE_PATTERN[bestMatch.code], counter,
this.MODULE_INDICES.bar);
bestMatch.correction.space = calculateCorrection(
self.CODE_PATTERN[bestMatch.code], counter,
this.MODULE_INDICES.space);
return bestMatch;
}
for ( j = 0; j < 4; j++) {
counter[j] = counter[j + 2];
}
counter[4] = 0;
counter[5] = 0;
counterPos--;
} else {
counterPos++;
}
counter[counterPos] = 1;
isWhite = !isWhite;
}
}
return null;
};
Code128Reader.prototype._decode = function() {
var self = this,
startInfo = self._findStart(),
code = null,
done = false,
result = [],
multiplier = 0,
checksum = 0,
codeset,
rawResult = [],
decodedCodes = [],
shiftNext = false,
unshift,
removeLastCharacter = true;
if (startInfo === null) {
return null;
}
code = {
code: startInfo.code,
start: startInfo.start,
end: startInfo.end,
correction: {
bar: startInfo.correction.bar,
space: startInfo.correction.space
}
};
decodedCodes.push(code);
checksum = code.code;
switch (code.code) {
case self.START_CODE_A:
codeset = self.CODE_A;
break;
case self.START_CODE_B:
codeset = self.CODE_B;
break;
case self.START_CODE_C:
codeset = self.CODE_C;
break;
default:
return null;
}
while (!done) {
unshift = shiftNext;
shiftNext = false;
code = self._decodeCode(code.end, code.correction);
if (code !== null) {
if (code.code !== self.STOP_CODE) {
removeLastCharacter = true;
}
if (code.code !== self.STOP_CODE) {
rawResult.push(code.code);
multiplier++;
checksum += multiplier * code.code;
}
decodedCodes.push(code);
switch (codeset) {
case self.CODE_A:
if (code.code < 64) {
result.push(String.fromCharCode(32 + code.code));
} else if (code.code < 96) {
result.push(String.fromCharCode(code.code - 64));
} else {
if (code.code !== self.STOP_CODE) {
removeLastCharacter = false;
}
switch (code.code) {
case self.CODE_SHIFT:
shiftNext = true;
codeset = self.CODE_B;
break;
case self.CODE_B:
codeset = self.CODE_B;
break;
case self.CODE_C:
codeset = self.CODE_C;
break;
case self.STOP_CODE:
done = true;
break;
}
}
break;
case self.CODE_B:
if (code.code < 96) {
result.push(String.fromCharCode(32 + code.code));
} else {
if (code.code !== self.STOP_CODE) {
removeLastCharacter = false;
}
switch (code.code) {
case self.CODE_SHIFT:
shiftNext = true;
codeset = self.CODE_A;
break;
case self.CODE_A:
codeset = self.CODE_A;
break;
case self.CODE_C:
codeset = self.CODE_C;
break;
case self.STOP_CODE:
done = true;
break;
}
}
break;
case self.CODE_C:
if (code.code < 100) {
result.push(code.code < 10 ? "0" + code.code : code.code);
} else {
if (code.code !== self.STOP_CODE) {
removeLastCharacter = false;
}
switch (code.code) {
case self.CODE_A:
codeset = self.CODE_A;
break;
case self.CODE_B:
codeset = self.CODE_B;
break;
case self.STOP_CODE:
done = true;
break;
}
}
break;
}
} else {
done = true;
}
if (unshift) {
codeset = codeset === self.CODE_A ? self.CODE_B : self.CODE_A;
}
}
if (code === null) {
return null;
}
code.end = self._nextUnset(self._row, code.end);
if (!self._verifyTrailingWhitespace(code)){
return null;
}
checksum -= multiplier * rawResult[rawResult.length - 1];
if (checksum % 103 !== rawResult[rawResult.length - 1]) {
return null;
}
if (!result.length) {
return null;
}
// remove last code from result (checksum)
if (removeLastCharacter) {
result.splice(result.length - 1, 1);
}
return {
code: result.join(""),
start: startInfo.start,
end: code.end,
codeset: codeset,
startInfo: startInfo,
decodedCodes: decodedCodes,
endInfo: code
};
};
BarcodeReader.prototype._verifyTrailingWhitespace = function(endInfo) {
var self = this,
trailingWhitespaceEnd;
trailingWhitespaceEnd = endInfo.end + ((endInfo.end - endInfo.start) / 2);
if (trailingWhitespaceEnd < self._row.length) {
if (self._matchRange(endInfo.end, trailingWhitespaceEnd, 0)) {
return endInfo;
}
}
return null;
};
function calculateCorrection(expected, normalized, indices) {
var length = indices.length,
sumNormalized = 0,
sumExpected = 0;
while(length--) {
sumExpected += expected[indices[length]];
sumNormalized += normalized[indices[length]];
}
return sumExpected/sumNormalized;
}
export default Code128Reader;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save