feat: refactor with ts
parent
2737d1970b
commit
d55e4ddcef
@ -1,22 +1,71 @@
|
||||
{
|
||||
"presets": [
|
||||
["env",
|
||||
[
|
||||
"@babel/env",
|
||||
{
|
||||
"modules": false,
|
||||
"targets": {
|
||||
"browsers": "last 2 versions, > 1%, ie >= 11",
|
||||
"node": "8.0"
|
||||
"browsers": [
|
||||
">0.25%"
|
||||
]
|
||||
},
|
||||
"modules": false,
|
||||
"loose": false
|
||||
}],
|
||||
"stage-2"
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
],
|
||||
"@babel/typescript"
|
||||
],
|
||||
"env": {
|
||||
"development": {
|
||||
"plugins": [
|
||||
"@babel/transform-runtime"
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"plugins": [
|
||||
"@babel/transform-runtime",
|
||||
"transform-remove-console"
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"presets": [
|
||||
[
|
||||
"@babel/env",
|
||||
{
|
||||
"modules": "commonjs",
|
||||
"targets": {
|
||||
"node": "current"
|
||||
}
|
||||
}
|
||||
],
|
||||
"@babel/typescript"
|
||||
]
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
["transform-runtime", {
|
||||
"helpers": false,
|
||||
"polyfill": false,
|
||||
"regenerator": false,
|
||||
"moduleName": "babel-runtime"
|
||||
}]
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
"@babel/plugin-syntax-import-meta",
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-proposal-json-strings",
|
||||
[
|
||||
"@babel/plugin-proposal-decorators",
|
||||
{
|
||||
"legacy": true
|
||||
}
|
||||
],
|
||||
"@babel/plugin-proposal-function-sent",
|
||||
"@babel/plugin-proposal-export-namespace-from",
|
||||
"@babel/plugin-proposal-numeric-separator",
|
||||
"@babel/plugin-proposal-throw-expressions",
|
||||
"@babel/plugin-proposal-export-default-from",
|
||||
"@babel/plugin-proposal-logical-assignment-operators",
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
[
|
||||
"@babel/plugin-proposal-pipeline-operator",
|
||||
{
|
||||
"proposal": "minimal"
|
||||
}
|
||||
],
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
"@babel/plugin-proposal-do-expressions"
|
||||
]
|
||||
}
|
@ -1,12 +1,7 @@
|
||||
root = true
|
||||
|
||||
[{*.js,*.css,*.html}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
[*]
|
||||
insert_final_newline = true
|
||||
|
||||
[{package.json,.*rc,*.yml}]
|
||||
[*.{js,ts}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
charset = utf-8
|
@ -1 +0,0 @@
|
||||
test
|
@ -1,45 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
},
|
||||
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
node: true,
|
||||
},
|
||||
|
||||
extends: ['airbnb-base'],
|
||||
|
||||
rules: {
|
||||
'no-shadow': [
|
||||
'error',
|
||||
{
|
||||
allow: ['state'],
|
||||
},
|
||||
],
|
||||
// 'import/extensions': 'off',
|
||||
'import/extensions': [
|
||||
'error',
|
||||
'always',
|
||||
{
|
||||
js: 'never',
|
||||
},
|
||||
],
|
||||
'import/no-unresolved': 'off',
|
||||
'no-param-reassign': 'off',
|
||||
'consistent-return': 'off',
|
||||
'global-require': 'off',
|
||||
'import/no-dynamic-require': 'off',
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
|
||||
// 4 行空格缩进
|
||||
indent: ['error', 4, { SwitchCase: 1 }],
|
||||
|
||||
'max-len': ['error', { code: 150 }],
|
||||
|
||||
'operator-linebreak': 0,
|
||||
},
|
||||
};
|
@ -1,3 +1,11 @@
|
||||
node_modules
|
||||
.DS_STORE
|
||||
lerna-debug.log
|
||||
yarn-error.log
|
||||
dist
|
||||
.DS_Store
|
||||
includes
|
||||
tsconfig.tsbuildinfo
|
||||
.env
|
||||
coverage
|
||||
yarn.lock
|
||||
.rpt2_cache
|
||||
|
@ -1,11 +1,22 @@
|
||||
node_modules
|
||||
config
|
||||
demo
|
||||
src
|
||||
*.map
|
||||
tests
|
||||
test
|
||||
.babelrc
|
||||
__tests__
|
||||
__test__
|
||||
.github
|
||||
.env*
|
||||
jest.config.js
|
||||
.eslintrc.js
|
||||
.eslintignore
|
||||
.prettierignore
|
||||
prettier.config.js
|
||||
release.config.js
|
||||
commitlint.config.js
|
||||
.editorconfig
|
||||
.travis.yml
|
||||
CHANGELOG.md
|
||||
package-lock.json
|
||||
TODO.md
|
||||
lerna.json
|
||||
tsconfig.tsbuildinfo
|
||||
tsconfig.json
|
||||
tsconfig.base.json
|
||||
.rpt2_cache
|
||||
.vscode
|
||||
|
@ -0,0 +1,9 @@
|
||||
|
||||
|
||||
# Development folders and files #
|
||||
#################################
|
||||
.tmp/
|
||||
node_modules/
|
||||
package.json
|
||||
.travis.yml
|
||||
dist
|
@ -0,0 +1,14 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"arrowParens": "always",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.json",
|
||||
"options": {
|
||||
"parser": "json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "8"
|
@ -1 +0,0 @@
|
||||
# Change Log
|
@ -1,52 +0,0 @@
|
||||
// rollup.config.js
|
||||
|
||||
import babel from 'rollup-plugin-babel';
|
||||
import nodeResolve from 'rollup-plugin-node-resolve';
|
||||
import commonjs from 'rollup-plugin-commonjs';
|
||||
import { uglify } from 'rollup-plugin-uglify';
|
||||
import common from './rollup';
|
||||
|
||||
const prod = process.env.NODE_ENV;
|
||||
|
||||
export default {
|
||||
input: 'src/index.js',
|
||||
output: {
|
||||
file: prod ? 'dist/index.aio.min.js' : 'dist/index.aio.js',
|
||||
format: 'umd',
|
||||
// 如果不同时使用 export 与 export default 可打开legacy
|
||||
// legacy: true,
|
||||
// name: common.name,
|
||||
name: 'QrcodeDecoder',
|
||||
banner: common.banner,
|
||||
},
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
main: true,
|
||||
}),
|
||||
commonjs({
|
||||
include: 'node_modules/**',
|
||||
}),
|
||||
babel({
|
||||
runtimeHelpers: true,
|
||||
exclude: 'node_modules/**',
|
||||
}),
|
||||
prod &&
|
||||
uglify({
|
||||
compress: {
|
||||
drop_debugger: true,
|
||||
drop_console: true,
|
||||
},
|
||||
output: {
|
||||
comments: (node, comment) => {
|
||||
if (comment.type === 'comment2') {
|
||||
// multiline comment
|
||||
return /@preserve|@license|@cc_on/i.test(
|
||||
comment.value,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
@ -1,7 +0,0 @@
|
||||
import config from './rollup.config';
|
||||
|
||||
// ES output
|
||||
config.output.format = 'es';
|
||||
config.output.file = 'dist/index.esm.js';
|
||||
|
||||
export default config;
|
@ -1,21 +0,0 @@
|
||||
// rollup.config.js
|
||||
|
||||
import babel from 'rollup-plugin-babel';
|
||||
import common from './rollup';
|
||||
|
||||
export default {
|
||||
input: 'src/index.js',
|
||||
output: {
|
||||
file: 'dist/index.js',
|
||||
format: 'cjs',
|
||||
// 如果不同时使用 export 与 export default 可打开legacy
|
||||
// legacy: true,
|
||||
banner: common.banner,
|
||||
},
|
||||
plugins: [
|
||||
babel({
|
||||
runtimeHelpers: true,
|
||||
exclude: 'node_modules/**',
|
||||
}),
|
||||
],
|
||||
};
|
@ -1,18 +0,0 @@
|
||||
const pkg = require('../package.json');
|
||||
|
||||
// 兼容 qrcode-decoder 和 @yugasun/qrcode-decoder
|
||||
const name = pkg.name.split('/').pop();
|
||||
const { version } = pkg;
|
||||
|
||||
const banner = `/* @preserve
|
||||
* qrcode-decoder ${version} (https://github.com/yugasun/qrcode-decoder)
|
||||
* API https://github.com/yugasun/qrcode-decoder/blob/master/doc/api.md
|
||||
* Copyright 2017-${new Date().getFullYear()} yugasun. All Rights Reserved
|
||||
* Licensed under MIT (https://github.com/yugasun/qrcode-decoder/blob/master/LICENSE)
|
||||
*/
|
||||
`;
|
||||
|
||||
export default {
|
||||
name,
|
||||
banner,
|
||||
};
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
verbose: true,
|
||||
testTimeout: 60000,
|
||||
transform: {
|
||||
'^.+\\.tsx?$': 'ts-jest',
|
||||
},
|
||||
testRegex: '(/__tests__/.*\\.(test|spec))\\.tsx?$',
|
||||
testPathIgnorePatterns: ['/node_modules/', '/dist/', '/demo/'],
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
};
|
@ -0,0 +1,127 @@
|
||||
import * as path from 'path';
|
||||
import * as rollup from 'rollup';
|
||||
import resolve from 'rollup-plugin-node-resolve';
|
||||
import typescript2 from 'rollup-plugin-typescript2';
|
||||
import { uglify } from 'rollup-plugin-uglify';
|
||||
import globals from 'rollup-plugin-node-globals';
|
||||
import commonjs from 'rollup-plugin-commonjs';
|
||||
import ts from 'typescript';
|
||||
|
||||
import { createLogger } from './logger';
|
||||
|
||||
const logBundle = createLogger('bundle');
|
||||
|
||||
const plugins: rollup.Plugin[] = [
|
||||
resolve({
|
||||
browser: true,
|
||||
jsnext: true,
|
||||
preferBuiltins: true,
|
||||
}),
|
||||
commonjs(),
|
||||
typescript2({
|
||||
tsconfig: path.join(__dirname, '..', 'tsconfig.json'),
|
||||
typescript: ts, // ensure we're using the same typescript (3.x) for rollup as for regular builds etc
|
||||
tsconfigOverride: {
|
||||
module: 'esnext',
|
||||
stripInternal: true,
|
||||
emitDeclarationOnly: false,
|
||||
composite: false,
|
||||
declaration: false,
|
||||
declarationMap: false,
|
||||
sourceMap: false,
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
async function bundleEsm() {
|
||||
const bundle = await rollup.rollup({
|
||||
input: path.join(process.cwd(), 'src/index.ts'),
|
||||
plugins: [...plugins, globals()],
|
||||
external: ['jsqr'],
|
||||
});
|
||||
await bundle.write({
|
||||
file: 'dist/index.esm.js',
|
||||
name: 'QrcodeDecoder',
|
||||
format: 'esm',
|
||||
sourcemap: false,
|
||||
globals: {
|
||||
jsqr: 'jsqr',
|
||||
tslib: 'tslib',
|
||||
},
|
||||
});
|
||||
}
|
||||
async function bundleUmd() {
|
||||
const bundle = await rollup.rollup({
|
||||
input: path.join(process.cwd(), 'src/index.ts'),
|
||||
plugins: [...plugins],
|
||||
external: ['jsqr'],
|
||||
});
|
||||
await bundle.write({
|
||||
file: 'dist/index.js',
|
||||
exports: 'named',
|
||||
name: 'QrcodeDecoder',
|
||||
format: 'umd',
|
||||
sourcemap: false,
|
||||
globals: {
|
||||
jsqr: 'jsqr',
|
||||
tslib: 'tslib',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function bundleAio() {
|
||||
const bundle = await rollup.rollup({
|
||||
input: path.join(process.cwd(), 'src/index.ts'),
|
||||
plugins: [
|
||||
...plugins,
|
||||
uglify({
|
||||
compress: {
|
||||
drop_debugger: true,
|
||||
drop_console: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
await bundle.write({
|
||||
file: 'dist/index.min.js',
|
||||
name: 'QrcodeDecoder',
|
||||
format: 'iife',
|
||||
sourcemap: false,
|
||||
});
|
||||
}
|
||||
|
||||
async function bundle() {
|
||||
try {
|
||||
const outputs = process.argv.slice(2)[0].split(',');
|
||||
logBundle(`Creating bundle`);
|
||||
|
||||
if (outputs.indexOf('esm') === -1) {
|
||||
logBundle(`Skipping esm`);
|
||||
} else {
|
||||
logBundle(`Creating esm`);
|
||||
await bundleEsm();
|
||||
}
|
||||
|
||||
if (outputs.indexOf('umd') === -1) {
|
||||
logBundle(`Skipping umd`);
|
||||
} else {
|
||||
logBundle(`Creating umd`);
|
||||
|
||||
await bundleUmd();
|
||||
}
|
||||
|
||||
if (outputs.indexOf('aio') === -1) {
|
||||
logBundle(`Skipping umd aio`);
|
||||
} else {
|
||||
logBundle(`Creating umd aio`);
|
||||
|
||||
await bundleAio();
|
||||
}
|
||||
} catch (err) {
|
||||
logBundle('Failed to bundle:');
|
||||
logBundle(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
bundle();
|
@ -0,0 +1,16 @@
|
||||
import * as l from 'fancy-log';
|
||||
const log = <typeof import('fancy-log')>(<any>(<any>l).default || l);
|
||||
import * as c from 'chalk';
|
||||
const chalk = <import('chalk').Chalk>(c.default || c);
|
||||
|
||||
export function createLogger(name: string): typeof log {
|
||||
const prefix = `> ${chalk.green(name)} `;
|
||||
const logger = <typeof log>log.bind(log, prefix);
|
||||
logger.info = log.info.bind(log, prefix);
|
||||
logger.dir = log.dir.bind(log, prefix);
|
||||
logger.warn = log.warn.bind(log, prefix);
|
||||
logger.error = log.error.bind(log, prefix);
|
||||
return logger;
|
||||
}
|
||||
|
||||
export { chalk as c };
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"module": "commonjs",
|
||||
"target": "es2015",
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
@ -1,376 +0,0 @@
|
||||
import 'babel-polyfill';
|
||||
import jsQR from 'jsqr';
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
class QrcodeDecoder {
|
||||
/**
|
||||
* Constructor for QrcodeDecoder
|
||||
*/
|
||||
constructor() {
|
||||
this.timerCapture = null;
|
||||
this.canvasElem = null;
|
||||
this.gCtx = null;
|
||||
this.stream = null;
|
||||
this.videoElem = null;
|
||||
this.getUserMediaHandler = null;
|
||||
this.videoConstraints = { video: true, audio: false };
|
||||
|
||||
this.defaultOption = { inversionAttempts: 'attemptBoth' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if canvas element is supported.
|
||||
*/
|
||||
/* eslint-disable class-methods-use-this */
|
||||
isCanvasSupported() {
|
||||
const elem = document.createElement('canvas');
|
||||
return !!(elem.getContext && elem.getContext('2d'));
|
||||
}
|
||||
|
||||
/**
|
||||
* draw lint
|
||||
*
|
||||
* @param {object} begin line begin point
|
||||
* @param {object} end line end point
|
||||
* @param {string} color color string
|
||||
*/
|
||||
// _drawLine(begin, end, color) {
|
||||
// this.gCtx.beginPath();
|
||||
// this.gCtx.moveTo(begin.x, begin.y);
|
||||
// this.gCtx.lineTo(end.x, end.y);
|
||||
// this.gCtx.lineWidth = 4;
|
||||
// this.gCtx.strokeStyle = color;
|
||||
// this.gCtx.stroke();
|
||||
// }
|
||||
|
||||
/**
|
||||
* create qrcode marker
|
||||
*
|
||||
* @param {object} code jsqr parse code object
|
||||
*/
|
||||
// _createQrcodeMark(code) {
|
||||
// this._drawLine(
|
||||
// code.location.topLeftCorner,
|
||||
// code.location.topRightCorner,
|
||||
// '#FF3B58',
|
||||
// );
|
||||
// this._drawLine(
|
||||
// code.location.topRightCorner,
|
||||
// code.location.bottomRightCorner,
|
||||
// '#FF3B58',
|
||||
// );
|
||||
// this._drawLine(
|
||||
// code.location.bottomRightCorner,
|
||||
// code.location.bottomLeftCorner,
|
||||
// '#FF3B58',
|
||||
// );
|
||||
// this._drawLine(
|
||||
// code.location.bottomLeftCorner,
|
||||
// code.location.topLeftCorner,
|
||||
// '#FF3B58',
|
||||
// );
|
||||
// }
|
||||
|
||||
_createImageData(target, width, height) {
|
||||
if (!this.canvasElem) {
|
||||
this._prepareCanvas(width, height);
|
||||
}
|
||||
|
||||
this.gCtx.clearRect(0, 0, width, height);
|
||||
this.gCtx.drawImage(target, 0, 0, width, height);
|
||||
|
||||
const imageData = this.gCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
this.canvasElem.width,
|
||||
this.canvasElem.height,
|
||||
);
|
||||
|
||||
return imageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the canvas element (which will
|
||||
* receive the image from the camera and provide
|
||||
* what the algorithm needs for checking for a
|
||||
* QRCode and then decoding it.)
|
||||
*
|
||||
*
|
||||
* @param {DOMElement} canvasElem the canvas
|
||||
* element
|
||||
* @param {number} width The width that
|
||||
* the canvas element
|
||||
* should have
|
||||
* @param {number} height The height that
|
||||
* the canvas element
|
||||
* should have
|
||||
* @return {DOMElement} the canvas
|
||||
* after the resize if width and height
|
||||
* provided.
|
||||
*/
|
||||
_prepareCanvas(width, height) {
|
||||
if (!this.canvasElem) {
|
||||
this.canvasElem = document.createElement('canvas');
|
||||
this.canvasElem.style.width = `${width}px`;
|
||||
this.canvasElem.style.height = `${height}px`;
|
||||
this.canvasElem.width = width;
|
||||
this.canvasElem.height = height;
|
||||
}
|
||||
|
||||
this.gCtx = this.canvasElem.getContext('2d');
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on the video dimensions and the canvas
|
||||
* that was previously generated captures the
|
||||
* video/image source and then paints into the
|
||||
* canvas so that the decoder is able to work as
|
||||
* it expects.
|
||||
* @param {DOMElement} videoElem <video> dom element
|
||||
* @param {Object} options options (optional) - Additional options.
|
||||
* inversionAttempts - (attemptBoth (default), dontInvert, onlyInvert, or invertFirst)
|
||||
* refer to jsqr options: https://github.com/cozmo/jsQR
|
||||
*/
|
||||
async _captureToCanvas(videoElem, options) {
|
||||
if (this.timerCapture) {
|
||||
clearTimeout(this.timerCapture);
|
||||
}
|
||||
const proms = () => {
|
||||
const p = new Promise(async (resolve) => {
|
||||
let code;
|
||||
if (videoElem.videoWidth && videoElem.videoHeight) {
|
||||
const imageData = this._createImageData(
|
||||
videoElem,
|
||||
videoElem.videoWidth,
|
||||
videoElem.videoHeight,
|
||||
);
|
||||
|
||||
code = jsQR(
|
||||
imageData.data,
|
||||
imageData.width,
|
||||
imageData.height,
|
||||
options,
|
||||
);
|
||||
|
||||
if (code) {
|
||||
resolve(code);
|
||||
} else {
|
||||
this.timerCapture = setTimeout(async () => {
|
||||
code = await this._captureToCanvas(
|
||||
videoElem,
|
||||
options,
|
||||
);
|
||||
resolve(code);
|
||||
}, 500);
|
||||
}
|
||||
} else {
|
||||
this.timerCapture = setTimeout(async () => {
|
||||
code = await this._captureToCanvas(videoElem, options);
|
||||
resolve(code);
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
return p;
|
||||
};
|
||||
|
||||
const result = await proms();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the video element for receiving
|
||||
* camera's input. Releases a stream if there
|
||||
* was any (resets).
|
||||
*
|
||||
* @param {DOMElement} videoElem <video> dom element
|
||||
* @param {Object} options options (optional) - Additional options.
|
||||
* inversionAttempts - (attemptBoth (default), dontInvert, onlyInvert, or invertFirst)
|
||||
* refer to jsqr options: https://github.com/cozmo/jsQR
|
||||
*/
|
||||
async decodeFromCamera(videoElem, options = {}) {
|
||||
const opts = {
|
||||
...this.defaultOption,
|
||||
...options,
|
||||
};
|
||||
|
||||
this.stop();
|
||||
if (!navigator.mediaDevices.getUserMedia) {
|
||||
throw new Error("Couldn't get video from camera");
|
||||
}
|
||||
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia(
|
||||
this.videoConstraints,
|
||||
);
|
||||
videoElem.srcObject = stream;
|
||||
// videoElem.src = window.URL.createObjectURL(stream);
|
||||
this.videoElem = videoElem;
|
||||
this.stream = stream;
|
||||
this.videoDimensions = false;
|
||||
|
||||
const code = await this.decodeFromVideo(videoElem, opts);
|
||||
return code;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the video element for video file.
|
||||
*
|
||||
* @param {DOMElement} videoElem <video> dom element
|
||||
* @param {Object} options options (optional) - Additional options.
|
||||
* inversionAttempts - (attemptBoth (default), dontInvert, onlyInvert, or invertFirst)
|
||||
* refer to jsqr options: https://github.com/cozmo/jsQR
|
||||
*/
|
||||
async decodeFromVideo(videoElem, options = {}) {
|
||||
const opts = {
|
||||
...this.defaultOption,
|
||||
...options,
|
||||
};
|
||||
try {
|
||||
this.videoElem = videoElem;
|
||||
const code = await this._captureToCanvas(videoElem, opts);
|
||||
return code;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes an image from its src.
|
||||
* @param {DOMElement} imageElem
|
||||
* @param {Object} options options (optional) - Additional options.
|
||||
* inversionAttempts - (attemptBoth (default), dontInvert, onlyInvert, or invertFirst)
|
||||
* refer to jsqr options: https://github.com/cozmo/jsQR
|
||||
*/
|
||||
async decodeFromImage(img, options = {}) {
|
||||
let imgDom;
|
||||
const opts = {
|
||||
...this.defaultOption,
|
||||
...options,
|
||||
};
|
||||
if (+img.nodeType > 0) {
|
||||
if (!img.src) {
|
||||
throw new Error('The ImageElement must contain a src');
|
||||
}
|
||||
imgDom = img;
|
||||
} else if (typeof img === 'string') {
|
||||
imgDom = document.createElement('img');
|
||||
imgDom.src = img;
|
||||
if (options.crossOrigin) {
|
||||
imgDom.crossOrigin = options.crossOrigin;
|
||||
}
|
||||
const proms = () => new Promise((resolve) => {
|
||||
imgDom.onload = () => resolve(true);
|
||||
});
|
||||
await proms();
|
||||
}
|
||||
|
||||
let code = false;
|
||||
if (imgDom) {
|
||||
code = this._decodeFromImageElm(imgDom, opts);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
_decodeFromImageElm(imgObj, options = {}) {
|
||||
const opts = {
|
||||
...this.defaultOption,
|
||||
...options,
|
||||
};
|
||||
const imageData = this._createImageData(
|
||||
imgObj,
|
||||
imgObj.width,
|
||||
imgObj.height,
|
||||
);
|
||||
|
||||
const code = jsQR(
|
||||
imageData.data,
|
||||
imageData.width,
|
||||
imageData.height,
|
||||
opts,
|
||||
);
|
||||
|
||||
if (code) {
|
||||
return code;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases a video stream that was being
|
||||
* captured by prepareToVideo
|
||||
*/
|
||||
stop() {
|
||||
if (this.stream) {
|
||||
const track = this.stream.getTracks()[0];
|
||||
track.stop();
|
||||
this.stream = undefined;
|
||||
|
||||
// fix: clear black bg after camera capture
|
||||
this.videoElem.srcObject = null;
|
||||
}
|
||||
|
||||
if (this.timerCapture) {
|
||||
clearTimeout(this.timerCapture);
|
||||
this.timerCapture = undefined;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sourceId for the camera to use.
|
||||
*
|
||||
* The sourceId can be found using the
|
||||
* getVideoSources function on a browser that
|
||||
* supports it (currently only Chrome).
|
||||
*
|
||||
* @param {String} sourceId The id of the
|
||||
* video source you want to use (or false to use
|
||||
* the current default)
|
||||
*/
|
||||
setSourceId(sourceId) {
|
||||
if (sourceId) {
|
||||
this.videoConstraints.video = {
|
||||
optional: [{ sourceId }],
|
||||
};
|
||||
} else {
|
||||
this.videoConstraints.video = true;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all available video sources on
|
||||
* the current device.
|
||||
* @param {Function} cb callback to be resolved
|
||||
* with error (first param) ou results (second
|
||||
* param) - a list containing all of the sources
|
||||
* that are of the 'video' kind.
|
||||
*/
|
||||
getVideoSources() {
|
||||
const sources = [];
|
||||
|
||||
if (!(MediaStreamTrack && MediaStreamTrack.getSources)) {
|
||||
throw new Error(
|
||||
'Current browser doest not support MediaStreamTrack.getSources',
|
||||
);
|
||||
}
|
||||
|
||||
MediaStreamTrack.getSources((sourceInfos) => {
|
||||
sourceInfos.forEach((sourceInfo) => {
|
||||
if (sourceInfo.kind === 'video') {
|
||||
sources.push(sourceInfo);
|
||||
}
|
||||
});
|
||||
});
|
||||
return sources;
|
||||
}
|
||||
}
|
||||
|
||||
export default QrcodeDecoder;
|
@ -0,0 +1,320 @@
|
||||
import { Options } from 'jsqr';
|
||||
import jsQR from 'jsqr';
|
||||
|
||||
class QrcodeDecoder {
|
||||
timerCapture: null | NodeJS.Timeout;
|
||||
canvasElem: null | HTMLCanvasElement;
|
||||
gCtx: null | CanvasRenderingContext2D;
|
||||
stream: null | MediaStream;
|
||||
videoElem: null | HTMLVideoElement;
|
||||
getUserMediaHandler: null;
|
||||
videoConstraints: MediaStreamConstraints;
|
||||
defaultOption: Options;
|
||||
|
||||
constructor() {
|
||||
this.timerCapture = null;
|
||||
this.canvasElem = null;
|
||||
this.gCtx = null;
|
||||
this.stream = null;
|
||||
this.videoElem = null;
|
||||
this.getUserMediaHandler = null;
|
||||
this.videoConstraints = {
|
||||
video: true,
|
||||
audio: false,
|
||||
};
|
||||
|
||||
this.defaultOption = { inversionAttempts: 'attemptBoth' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if canvas element is supported.
|
||||
*/
|
||||
isCanvasSupported() {
|
||||
const elem = document.createElement('canvas');
|
||||
return !!(elem.getContext && elem.getContext('2d'));
|
||||
}
|
||||
|
||||
_createImageData(target: CanvasImageSource, width: number, height: number) {
|
||||
if (!this.canvasElem) {
|
||||
this._prepareCanvas(width, height);
|
||||
}
|
||||
|
||||
this.gCtx!.clearRect(0, 0, width, height);
|
||||
this.gCtx!.drawImage(target, 0, 0, width, height);
|
||||
|
||||
const imageData = this.gCtx!.getImageData(
|
||||
0,
|
||||
0,
|
||||
this.canvasElem!.width,
|
||||
this.canvasElem!.height,
|
||||
);
|
||||
|
||||
return imageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the canvas element (which will
|
||||
* receive the image from the camera and provide
|
||||
* what the algorithm needs for checking for a
|
||||
* QRCode and then decoding it.)
|
||||
*
|
||||
*
|
||||
* @param {DOMElement} canvasElem the canvas
|
||||
* element
|
||||
* @param {number} width The width that
|
||||
* the canvas element
|
||||
* should have
|
||||
* @param {number} height The height that
|
||||
* the canvas element
|
||||
* should have
|
||||
* @return {DOMElement} the canvas
|
||||
* after the resize if width and height
|
||||
* provided.
|
||||
*/
|
||||
_prepareCanvas(width: number, height: number) {
|
||||
if (!this.canvasElem) {
|
||||
this.canvasElem = document.createElement('canvas');
|
||||
this.canvasElem.style.width = `${width}px`;
|
||||
this.canvasElem.style.height = `${height}px`;
|
||||
this.canvasElem.width = width;
|
||||
this.canvasElem.height = height;
|
||||
}
|
||||
|
||||
this.gCtx = this.canvasElem.getContext('2d');
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on the video dimensions and the canvas
|
||||
* that was previously generated captures the
|
||||
* video/image source and then paints into the
|
||||
* canvas so that the decoder is able to work as
|
||||
* it expects.
|
||||
* @param {DOMElement} videoElem <video> dom element
|
||||
* @param {Object} options options (optional) - Additional options.
|
||||
* inversionAttempts - (attemptBoth (default), dontInvert, onlyInvert, or invertFirst)
|
||||
* refer to jsqr options: https://github.com/cozmo/jsQR
|
||||
*/
|
||||
async _captureToCanvas(videoElem: HTMLVideoElement, options: Options) {
|
||||
if (this.timerCapture) {
|
||||
clearTimeout(this.timerCapture);
|
||||
}
|
||||
const proms = () => {
|
||||
const p = new Promise(async (resolve) => {
|
||||
let code;
|
||||
if (videoElem.videoWidth && videoElem.videoHeight) {
|
||||
const imageData = this._createImageData(
|
||||
videoElem,
|
||||
videoElem.videoWidth,
|
||||
videoElem.videoHeight,
|
||||
);
|
||||
|
||||
code = jsQR(
|
||||
imageData.data,
|
||||
imageData.width,
|
||||
imageData.height,
|
||||
options,
|
||||
);
|
||||
|
||||
if (code) {
|
||||
resolve(code);
|
||||
} else {
|
||||
this.timerCapture = setTimeout(async () => {
|
||||
code = await this._captureToCanvas(videoElem, options);
|
||||
resolve(code);
|
||||
}, 500);
|
||||
}
|
||||
} else {
|
||||
this.timerCapture = setTimeout(async () => {
|
||||
code = await this._captureToCanvas(videoElem, options);
|
||||
resolve(code);
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
return p;
|
||||
};
|
||||
|
||||
const result = await proms();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the video element for receiving
|
||||
* camera's input. Releases a stream if there
|
||||
* was any (resets).
|
||||
*
|
||||
* @param {DOMElement} videoElem <video> dom element
|
||||
* @param {Object} options options (optional) - Additional options.
|
||||
* inversionAttempts - (attemptBoth (default), dontInvert, onlyInvert, or invertFirst)
|
||||
* refer to jsqr options: https://github.com/cozmo/jsQR
|
||||
*/
|
||||
async decodeFromCamera(videoElem: HTMLVideoElement, options = {}) {
|
||||
const opts = {
|
||||
...this.defaultOption,
|
||||
...options,
|
||||
};
|
||||
|
||||
this.stop();
|
||||
if (!navigator.mediaDevices.getUserMedia) {
|
||||
throw new Error("Couldn't get video from camera");
|
||||
}
|
||||
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia(
|
||||
this.videoConstraints,
|
||||
);
|
||||
videoElem.srcObject = stream;
|
||||
// videoElem.src = window.URL.createObjectURL(stream);
|
||||
this.videoElem = videoElem;
|
||||
this.stream = stream;
|
||||
|
||||
const code = await this.decodeFromVideo(videoElem, opts);
|
||||
return code;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the video element for video file.
|
||||
*
|
||||
* @param {DOMElement} videoElem <video> dom element
|
||||
* @param {Object} options options (optional) - Additional options.
|
||||
* inversionAttempts - (attemptBoth (default), dontInvert, onlyInvert, or invertFirst)
|
||||
* refer to jsqr options: https://github.com/cozmo/jsQR
|
||||
*/
|
||||
async decodeFromVideo(videoElem: HTMLVideoElement, options = {}) {
|
||||
const opts = {
|
||||
...this.defaultOption,
|
||||
...options,
|
||||
};
|
||||
try {
|
||||
this.videoElem = videoElem;
|
||||
const code = await this._captureToCanvas(videoElem, opts);
|
||||
return code;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes an image from its src.
|
||||
* @param {DOMElement} imageElem
|
||||
* @param {Object} options options (optional) - Additional options.
|
||||
* inversionAttempts - (attemptBoth (default), dontInvert, onlyInvert, or invertFirst)
|
||||
* refer to jsqr options: https://github.com/cozmo/jsQR
|
||||
*/
|
||||
async decodeFromImage(
|
||||
img: HTMLImageElement,
|
||||
options: { crossOrigin?: string } = {},
|
||||
) {
|
||||
let imgDom: HTMLImageElement | null = null;
|
||||
const opts = {
|
||||
...this.defaultOption,
|
||||
...options,
|
||||
};
|
||||
if (+img.nodeType > 0) {
|
||||
if (!img.src) {
|
||||
throw new Error('The ImageElement must contain a src');
|
||||
}
|
||||
imgDom = img;
|
||||
} else if (typeof img === 'string') {
|
||||
imgDom = document.createElement('img');
|
||||
imgDom.src = img;
|
||||
if (options.crossOrigin) {
|
||||
imgDom.crossOrigin = options.crossOrigin;
|
||||
}
|
||||
const proms = () =>
|
||||
new Promise((resolve) => {
|
||||
imgDom!.onload = () => resolve(true);
|
||||
});
|
||||
await proms();
|
||||
}
|
||||
|
||||
let code = null;
|
||||
if (imgDom) {
|
||||
code = this._decodeFromImageElm(imgDom, opts);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
_decodeFromImageElm(imgObj: HTMLImageElement, options = {}) {
|
||||
const opts: Options = {
|
||||
...this.defaultOption,
|
||||
...options,
|
||||
};
|
||||
const imageData = this._createImageData(
|
||||
imgObj,
|
||||
imgObj.width,
|
||||
imgObj.height,
|
||||
);
|
||||
|
||||
const code = jsQR(imageData.data, imageData.width, imageData.height, opts);
|
||||
|
||||
if (code) {
|
||||
return code;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases a video stream that was being
|
||||
* captured by prepareToVideo
|
||||
*/
|
||||
stop() {
|
||||
if (this.stream) {
|
||||
const track = this.stream.getTracks()[0];
|
||||
track.stop();
|
||||
this.stream = null;
|
||||
|
||||
// fix: clear black bg after camera capture
|
||||
this.videoElem!.srcObject = null;
|
||||
}
|
||||
|
||||
if (this.timerCapture) {
|
||||
clearTimeout(this.timerCapture);
|
||||
this.timerCapture = null;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sourceId for the camera to use.
|
||||
*/
|
||||
setGroupId(groupId: string) {
|
||||
if (groupId) {
|
||||
this.videoConstraints.video = {
|
||||
advanced: [{ groupId }],
|
||||
};
|
||||
} else {
|
||||
this.videoConstraints.video = true;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all available video sources on
|
||||
* the current device.
|
||||
*/
|
||||
async getVideoDevices() {
|
||||
if (navigator.mediaDevices.enumerateDevices) {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return devices.filter((item: MediaDeviceInfo) => {
|
||||
if (item.kind === 'videoinput') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
throw new Error(
|
||||
'Current browser doest not support MediaStreamTrack.getSources',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default QrcodeDecoder;
|
@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"baseUrl": ".",
|
||||
"downlevelIteration": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"preserveConstEnums": true,
|
||||
"importHelpers": false,
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"lib": ["es5", "es6", "dom"],
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": false,
|
||||
"paths": {
|
||||
"*": ["typings/*", "includes/*"]
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"noUnusedLocals": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
make test for your own private key
|
@ -1,37 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Mocha</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="../../node_modules/mocha/mocha.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="mocha"></div>
|
||||
<script src="../../node_modules/es5-shim/es5-shim.js"></script>
|
||||
<script src="../../node_modules/es5-shim/es5-sham.js"></script>
|
||||
|
||||
<script src="../../node_modules/mocha/mocha.js"></script>
|
||||
<script src="../../node_modules/expect.js/index.js"></script>
|
||||
<script>
|
||||
mocha.setup('bdd');
|
||||
</script>
|
||||
<script src="../../dist/index.aio.js"></script>
|
||||
<script>
|
||||
var libs = {
|
||||
'expect.js': expect,
|
||||
'../dist/index.js': window['qrcode-decoder']
|
||||
};
|
||||
var require = function(path) {
|
||||
return libs[path];
|
||||
}
|
||||
</script>
|
||||
<script src="../test.js"></script>
|
||||
<script>
|
||||
mocha.run();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,57 +0,0 @@
|
||||
const expect = require('expect.js');
|
||||
const QrcodeDecoder = require('../dist/index.js');
|
||||
|
||||
describe('QrcodeDecoder', function () {
|
||||
// global.mocha.checkLeaks = false;
|
||||
this.timeout(180000);
|
||||
|
||||
it('be defined', function () {
|
||||
expect(QrcodeDecoder).to.be.ok();
|
||||
});
|
||||
|
||||
var qr;
|
||||
|
||||
beforeEach(function () {
|
||||
qr = new QrcodeDecoder();
|
||||
});
|
||||
|
||||
describe('decodeFromImage', function () {
|
||||
it('decode image from img element', function (done) {
|
||||
const img = document.createElement('img');
|
||||
img.src = 'demo/assets/qrcode.png';
|
||||
img.onload = async function () {
|
||||
const result = await qr.decodeFromImage(img);
|
||||
expect(result.data).to.equal('192.168.1.13:3000');
|
||||
done();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
it('decode image from img url', async function () {
|
||||
const result = await qr.decodeFromImage('demo/assets/qrcode.png');
|
||||
expect(result.data).to.equal('192.168.1.13:3000');
|
||||
});
|
||||
|
||||
it('throw if no src in image element', async function (done) {
|
||||
var img = document.createElement('img');
|
||||
try {
|
||||
await qr.decodeFromImage(img);
|
||||
} catch(e) {
|
||||
expect(e.message).to.equal('The ImageElement must contain a src')
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('decodeFromCamera', function () {
|
||||
it('decode from a video with qrcode', async function () {
|
||||
const video = document.createElement('video');
|
||||
video.setAttribute('autoplay', true);
|
||||
video.setAttribute('src', 'demo/assets/qrcode-video.mp4');
|
||||
|
||||
const result = await qr.decodeFromVideo(video);
|
||||
expect(result.data).to.equal('192.168.1.13:3000');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"downlevelIteration": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"preserveConstEnums": true,
|
||||
"importHelpers": false,
|
||||
"lib": ["esnext", "dom"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": false,
|
||||
"paths": {
|
||||
"*": ["typings/*", "includes/*"]
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"noUnusedLocals": true,
|
||||
"strict": true,
|
||||
"target": "es5"
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
{
|
||||
"extends": ["tslint:latest", "tslint-config-prettier"],
|
||||
"rules": {
|
||||
"interface-name": [true, "never-prefix"],
|
||||
"member-access": false,
|
||||
"no-angle-bracket-type-assertion": false,
|
||||
"no-bitwise": false,
|
||||
"no-console": false,
|
||||
"no-default-export": true,
|
||||
"no-empty-interface": false,
|
||||
"no-implicit-dependencies": false,
|
||||
"no-submodule-imports": false,
|
||||
"ordered-imports": [false],
|
||||
"object-literal-sort-keys": false,
|
||||
"object-literal-key-quotes": [true, "as-needed"],
|
||||
"quotemark": [true, "single"],
|
||||
"semicolon": [true, "always", "ignore-bound-class-methods"],
|
||||
"jsx-boolean-value": false
|
||||
},
|
||||
"linterOptions": {
|
||||
"exclude": ["config/**/*.js", "node_modules/**/*.ts"]
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue