mighty-webcamjs
Version:
HTML5 Webcam Image Capture Library with Flash Fallback
316 lines (280 loc) • 9.31 kB
JavaScript
var navigator = global.navigator || {};
var ua = navigator.userAgent || '';
var isBrowser = typeof window !== 'undefined' && (!ua.includes('Node.js') && !ua.includes('jsdom'));
var EXIF = require('exif-js');
var blurMeasureInspectorAsync = isBrowser ? require('inspector-bokeh/async') : Promise.resolve;
var localStorage;
try {
localStorage = global.localStorage;
} catch (e) {
// volatile storage for paranoia mode
localStorage = {
getItem: (name) => localStorage[name],
setItem: (name, value) => localStorage[name] = value,
};
}
function getAndroidVersion() {
var match = ua.match(/android\s([0-9\.]*)/i);
return match ? match[1] : false;
}
function getIOSVersion() {
return !global.MSStream && (function iOSversion() {
if (/iP(hone|od|ad)/.test(navigator.platform)) {
// supports iOS 2.0 and later: <http://bit.ly/TJjs1V>
var v = navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/) || [0, 0, 0];
return parseFloat(parseInt(v[1], 10) + '.' + parseInt(v[2], 10));
}
})();
}
function measureBlur(canvasData) {
return new Promise(function(resolve, reject) {
if (canvasData) {
blurMeasureInspectorAsync(canvasData)
.then(resolve, reject);
} else {
reject('No canvasData');
}
});
}
const debouncePromise = (func) => {
// debouncePromise takes function, that returns promise
// and makes sure - there is just one concurrent call of promise.
// NOTE: arguments might be ignored
let isPromiseRunning = false;
let lastPromise;
return (...args) => {
if (!isPromiseRunning) {
lastPromise = new Promise((resolve, reject) => {
isPromiseRunning = true;
const promise = func(...args);
const end = () => { isPromiseRunning = false; }
const resolver = (...promiseArgs) => (end(), resolve(...promiseArgs));
const rejector = (...promiseArgs) => (end(), reject(...promiseArgs));
promise.then(resolver, rejector);
});
}
return lastPromise;
};
};
const whenDOMReady = (() => {
// takes function, that can be called, when DOM is loaded.
let isDOMLoaded = false;
const domReadyPromise = new Promise((resolve) => {
if (isBrowser) {
document.addEventListener('DOMContentLoaded', resolve, false);
} else {
resolve();
}
});
domReadyPromise.then(() => { isDOMLoaded = true; });
return (context, func) => {
// on Chrome 52 @ android mediaDevices.getUserMedia is triggered with a delay.
// functions to be triggered when camera is ready
return (...args) => {
if (isDOMLoaded) {
// synchronous call
return func.apply(context, args);
}
// asynchronous call
return domReadyPromise.then(() => func.apply(context, args));
};
};
})();
function drawImageScaled(img, canvas) {
if (process.env.NODE_ENV !== 'production') {
console.assert(canvas instanceof HTMLCanvasElement, 'canvas passed to drawImageScaled needs to be a <canvas> element.');
console.assert(img instanceof HTMLImageElement || img instanceof Image || img instanceof HTMLCanvasElement, 'img passed to drawImageScaled need to be a <img> element.');
}
// stackoverflow.com/questions/23104582/scaling-an-image-to-fit-on-canvas#answer-23105310
var ctx = canvas.getContext('2d');
var hRatio = canvas.width / img.width;
var vRatio = canvas.height / img.height;
var ratio = Math.min( hRatio, vRatio );
var centerShift_x = ( canvas.width - img.width*ratio ) / 2;
var centerShift_y = ( canvas.height - img.height*ratio ) / 2;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, img.width, img.height, centerShift_x, centerShift_y, img.width*ratio, img.height*ratio);
}
function handleImageInput(rawFile) {
return new Promise((resolve, reject) => {
if (!rawFile) return reject();
var reader = new FileReader();
reader.onload = function(readerEvent) {
var img = new Image();
img.onload = function() {
resolve({ // NOTE: This is not ImageData object!
rawFile: rawFile,
data: img,
width: img.width,
height: img.height
});
};
img.onerror = reject;
img.src = readerEvent.target.result;
};
reader.readAsDataURL(rawFile);
});
}
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function setPrefixedStyle(element, cssProp, cssValue) {
if (!element) return;
const prefixes = ['o', 'ms', 'moz', 'webkit'];
prefixes.forEach((prefix) => {
element.style[prefix + capitalizeFirstLetter(cssProp)] = cssValue;
});
element.style[cssProp] = cssValue;
}
function detectFlash() {
// return true if browser supports flash, false otherwise
// Code snippet borrowed from: https://github.com/swfobject/swfobject
var SHOCKWAVE_FLASH = "Shockwave Flash",
SHOCKWAVE_FLASH_AX = "ShockwaveFlash.ShockwaveFlash",
FLASH_MIME_TYPE = "application/x-shockwave-flash",
hasFlash = false;
if (navigator.plugins !== undefined && typeof navigator.plugins[SHOCKWAVE_FLASH] === "object") {
var desc = navigator.plugins[SHOCKWAVE_FLASH].description;
if (desc && (navigator.mimeTypes !== undefined && navigator.mimeTypes[FLASH_MIME_TYPE] && navigator.mimeTypes[FLASH_MIME_TYPE].enabledPlugin)) {
hasFlash = true;
}
}
else if (typeof ActiveXObject !== "undefined") {
try {
var ax = new ActiveXObject(SHOCKWAVE_FLASH_AX);
if (ax) {
var ver = ax.GetVariable("$version");
if (ver) hasFlash = true;
}
}
catch (e) {}
}
return hasFlash;
}
function createElement(elementName, props) {
var element = document.createElement(elementName);
var uniqAttrs = ['innerHTML', 'className', 'width', 'height'];
if (props.style) {
Object.assign(element.style, props.style);
}
Object.keys(props).forEach(function(prop) {
if (prop === 'style') return;
if (uniqAttrs.includes(prop) || /^on/.test(prop)) {
element[prop] = props[prop];
} else {
element.setAttribute(prop, props[prop]);
}
});
return element;
}
function validateUploadedPhoto(img, params) {
// When selecting "Take a photo" instead of "Select from Library"
// on iOS device, not all Exif data are sent. This how we detect,
// photo was just taken.
// Android's don't allow to pick a photo from a library, so problem
// does not need to be addressed there.
// iOS 11+ has getUserMedia, but 3rdparty browsers like Chrome@iOS does not
// supprt that. EXIF verification does not work there.
return new Promise(function(resolve, reject) {
const iOSversion = getIOSVersion();
if (iOSversion && iOSversion < 11) {
EXIF.getData(img, function() {
var exif = EXIF.getAllTags(this) || {};
if (params.force_fresh_photo) {
if (
exif.Orientation &&
!exif.ExifVersion &&
!exif.DateTimeOriginal
) {
resolve(exif);
} else {
reject();
}
} else {
resolve(exif);
}
});
} else {
resolve();
}
});
}
function adjustUploadedPhoto(img, exif, params) {
if (process.env.NODE_ENV !== 'production') {
console.assert((img instanceof HTMLImageElement) || (img instanceof Image), 'img passed to adjustUploadedPhoto need to be a <img> element.');
exif && console.assert(exif instanceof Object, 'exif passed to adjustUploadedPhoto needs to be an object');
}
// This function resize and rotate photo if needed
//
// Read more, why rotation is required:
// - http://www.galloway.me.uk/2012/01/uiimageorientation-exif-orientation-sample-images/
// - http://sylvana.net/jpegcrop/exif_orientation.html
return new Promise(function(resolve, reject) {
var canvas = createElement('canvas', {});
var ctx = canvas.getContext("2d");
var width = img.width;
var height = img.height;
var longerSide = Math.max(width, height);
var longerDestSize = Math.max(params.dest_width, params.dest_height);
var scale = Math.min(longerDestSize / longerSide, 1);
width = parseInt(scale * img.width, 10);
height = parseInt(scale * img.height, 10);
canvas.width = width;
canvas.height = height;
if (getIOSVersion()) {
// rotate photo according to exif on iOS.
if (exif && exif.Orientation) {
if (exif.Orientation >= 5) {
canvas.width = height;
canvas.height = width;
}
var transformProps = [ // http://stackoverflow.com/a/31273162/2590921
[1, 0, 0, 1, 0, 0],
[-1, 0, 0, 1, width, 0],
[-1, 0, 0, -1, width, height],
[1, 0, 0, -1, 0, height],
[0, 1, 1, 0, 0, 0],
[0, 1, -1, 0, height, 0],
[0, -1, -1, 0, height, width],
[0, -1, 1, 0, 0, width]
];
ctx.transform.apply(ctx, transformProps[exif.Orientation - 1]);
} else {
console.warn('Invalid image orientation from EXIF:', img, exif);
}
}
ctx.drawImage(img, 0, 0, width, height);
resolve(canvas);
});
}
// gets device orientation.
// possible values: "landscape" | "portrait" | "" (can't detect)
function getDeviceOrientation() {
function mediaQuery(orientation) {
return `(orientation: ${orientation})`;
}
const orientations = ['landscape', 'portrait'];
for (let orientation of orientations) {
if (window.matchMedia(mediaQuery(orientation)).matches) {
return orientation;
}
}
return '';
}
module.exports = {
adjustUploadedPhoto,
createElement,
debouncePromise,
detectFlash,
drawImageScaled,
getAndroidVersion,
getIOSVersion,
handleImageInput,
localStorage,
measureBlur,
setPrefixedStyle,
validateUploadedPhoto,
whenDOMReady,
isBrowser,
getDeviceOrientation,
};