From 746100a761da2b87d50832233096619bdaa49ba3 Mon Sep 17 00:00:00 2001 From: Siddharth Bhalgami Date: Wed, 3 Jul 2019 11:13:50 +0800 Subject: [PATCH] [11.0][MIG] web_widget_image_webcam: update webcam library --- .../static/src/js/webcam.js | 412 ++++++++++++++++-- .../static/src/js/webcam.swf | Bin 6944 -> 7090 bytes 2 files changed, 373 insertions(+), 39 deletions(-) diff --git a/web_widget_image_webcam/static/src/js/webcam.js b/web_widget_image_webcam/static/src/js/webcam.js index 3edd3250..80af6891 100644 --- a/web_widget_image_webcam/static/src/js/webcam.js +++ b/web_widget_image_webcam/static/src/js/webcam.js @@ -1,23 +1,49 @@ -// WebcamJS v1.0.6 +// WebcamJS v1.0.25 // Webcam library for capturing JPEG/PNG images in JavaScript // Attempts getUserMedia, falls back to Flash // Author: Joseph Huckaby: http://github.com/jhuckaby // Based on JPEGCam: http://code.google.com/p/jpegcam/ -// Copyright (c) 2012 - 2015 Joseph Huckaby +// Copyright (c) 2012 - 2017 Joseph Huckaby // Licensed under the MIT License (function(window) { +var _userMedia; + +// declare error types + +// inheritance pattern here: +// https://stackoverflow.com/questions/783818/how-do-i-create-a-custom-error-in-javascript +function FlashError() { + var temp = Error.apply(this, arguments); + temp.name = this.name = "FlashError"; + this.stack = temp.stack; + this.message = temp.message; +} + +function WebcamError() { + var temp = Error.apply(this, arguments); + temp.name = this.name = "WebcamError"; + this.stack = temp.stack; + this.message = temp.message; +} + +var IntermediateInheritor = function() {}; +IntermediateInheritor.prototype = Error.prototype; + +FlashError.prototype = new IntermediateInheritor(); +WebcamError.prototype = new IntermediateInheritor(); var Webcam = { - version: '1.0.6', + version: '1.0.25', // globals protocol: location.protocol.match(/https/i) ? 'https' : 'http', - swfURL: '', // URI to webcam.swf movie (defaults to the js location) loaded: false, // true when webcam movie finishes loading live: false, // true when webcam is initialized and ready to snap userMedia: true, // true when getUserMedia is supported natively - + + iOS: /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream, + params: { width: 0, height: 0, @@ -25,11 +51,24 @@ var Webcam = { dest_height: 0, // these default to width/height image_format: 'jpeg', // image format (may be jpeg or png) jpeg_quality: 90, // jpeg image quality from 0 (worst) to 100 (best) + enable_flash: true, // enable flash fallback, force_flash: false, // force flash mode, flip_horiz: false, // flip image horiz (mirror mode) fps: 30, // camera frames per second upload_name: 'webcam', // name of file in upload post data - constraints: null // custom user media constraints + constraints: null, // custom user media constraints, + swfURL: '', // URI to webcam.swf movie (defaults to the js location) + flashNotDetectedText: 'ERROR: No Adobe Flash Player detected. Webcam.js relies on Flash for browsers that do not support getUserMedia (like yours).', + noInterfaceFoundText: 'No supported webcam interface found.', + unfreeze_snap: true, // Whether to unfreeze the camera after snap (defaults to true) + iosPlaceholderText: 'Click here to open camera.', + user_callback: null, // callback function for snapshot (used if no user_callback parameter given to snap function) + user_canvas: null // user provided canvas for snapshot (used if no user_canvas parameter given to snap function) + }, + + errors: { + FlashError: FlashError, + WebcamError: WebcamError }, hooks: {}, // callback hook functions @@ -53,6 +92,10 @@ var Webcam = { window.URL = window.URL || window.webkitURL || window.mozURL || window.msURL; this.userMedia = this.userMedia && !!this.mediaDevices && !!window.URL; + if (this.iOS) { + this.userMedia = null; + } + // Older versions of firefox (< 21) apparently claim support but user media does not actually work if (navigator.userAgent.match(/Firefox\D+(\d+)/)) { if (parseInt(RegExp.$1, 10) < 21) this.userMedia = null; @@ -66,6 +109,123 @@ var Webcam = { } }, + exifOrientation: function(binFile) { + // extract orientation information from the image provided by iOS + // algorithm based on exif-js + var dataView = new DataView(binFile); + if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) { + console.log('Not a valid JPEG file'); + return 0; + } + var offset = 2; + var marker = null; + while (offset < binFile.byteLength) { + // find 0xFFE1 (225 marker) + if (dataView.getUint8(offset) != 0xFF) { + console.log('Not a valid marker at offset ' + offset + ', found: ' + dataView.getUint8(offset)); + return 0; + } + marker = dataView.getUint8(offset + 1); + if (marker == 225) { + offset += 4; + var str = ""; + for (n = 0; n < 4; n++) { + str += String.fromCharCode(dataView.getUint8(offset+n)); + } + if (str != 'Exif') { + console.log('Not valid EXIF data found'); + return 0; + } + + offset += 6; // tiffOffset + var bigEnd = null; + + // test for TIFF validity and endianness + if (dataView.getUint16(offset) == 0x4949) { + bigEnd = false; + } else if (dataView.getUint16(offset) == 0x4D4D) { + bigEnd = true; + } else { + console.log("Not valid TIFF data! (no 0x4949 or 0x4D4D)"); + return 0; + } + + if (dataView.getUint16(offset+2, !bigEnd) != 0x002A) { + console.log("Not valid TIFF data! (no 0x002A)"); + return 0; + } + + var firstIFDOffset = dataView.getUint32(offset+4, !bigEnd); + if (firstIFDOffset < 0x00000008) { + console.log("Not valid TIFF data! (First offset less than 8)", dataView.getUint32(offset+4, !bigEnd)); + return 0; + } + + // extract orientation data + var dataStart = offset + firstIFDOffset; + var entries = dataView.getUint16(dataStart, !bigEnd); + for (var i=0; i 8) { + console.log('Invalid EXIF orientation value ('+value+')'); + return 0; + } + return value; + } + } + } else { + offset += 2+dataView.getUint16(offset+2); + } + } + return 0; + }, + + fixOrientation: function(origObjURL, orientation, targetImg) { + // fix image orientation based on exif orientation data + // exif orientation information + // http://www.impulseadventure.com/photo/exif-orientation.html + // link source wikipedia (https://en.wikipedia.org/wiki/Exif#cite_note-20) + var img = new Image(); + img.addEventListener('load', function(event) { + var canvas = document.createElement('canvas'); + var ctx = canvas.getContext('2d'); + + // switch width height if orientation needed + if (orientation < 5) { + canvas.width = img.width; + canvas.height = img.height; + } else { + canvas.width = img.height; + canvas.height = img.width; + } + + // transform (rotate) image - see link at beginning this method + switch (orientation) { + case 2: ctx.transform(-1, 0, 0, 1, img.width, 0); break; + case 3: ctx.transform(-1, 0, 0, -1, img.width, img.height); break; + case 4: ctx.transform(1, 0, 0, -1, 0, img.height); break; + case 5: ctx.transform(0, 1, 1, 0, 0, 0); break; + case 6: ctx.transform(0, 1, -1, 0, img.height , 0); break; + case 7: ctx.transform(0, -1, -1, 0, img.height, img.width); break; + case 8: ctx.transform(0, -1, 1, 0, 0, img.width); break; + } + + ctx.drawImage(img, 0, 0); + // pass rotated image data to the target image container + targetImg.src = canvas.toDataURL(); + }, false); + // start transformation by load event + img.src = origObjURL; + }, + attach: function(elem) { // create webcam preview and attach to DOM element // pass in actual DOM reference, ID, or CSS selector @@ -73,7 +233,7 @@ var Webcam = { elem = document.getElementById(elem) || document.querySelector(elem); } if (!elem) { - return this.dispatch('error', "Could not locate DOM element to attach to."); + return this.dispatch('error', new WebcamError("Could not locate DOM element to attach to.")); } this.container = elem; elem.innerHTML = ''; // start with empty element @@ -87,12 +247,21 @@ var Webcam = { if (!this.params.width) this.params.width = elem.offsetWidth; if (!this.params.height) this.params.height = elem.offsetHeight; + // make sure we have a nonzero width and height at this point + if (!this.params.width || !this.params.height) { + return this.dispatch('error', new WebcamError("No width and/or height for webcam. Please call set() first, or attach to a visible element.")); + } + // set defaults for dest_width / dest_height if not set if (!this.params.dest_width) this.params.dest_width = this.params.width; if (!this.params.dest_height) this.params.dest_height = this.params.height; + this.userMedia = _userMedia === undefined ? this.userMedia : _userMedia; // if force_flash is set, disable userMedia - if (this.params.force_flash) this.userMedia = null; + if (this.params.force_flash) { + _userMedia = this.userMedia; + this.userMedia = null; + } // check for default fps if (typeof this.params.fps !== "number") this.params.fps = 30; @@ -139,25 +308,148 @@ var Webcam = { }) .then( function(stream) { // got access, attach stream to video - video.src = window.URL.createObjectURL( stream ) || stream; - self.stream = stream; - self.loaded = true; - self.live = true; - self.dispatch('load'); - self.dispatch('live'); - self.flip(); + video.onloadedmetadata = function(e) { + self.stream = stream; + self.loaded = true; + self.live = true; + self.dispatch('load'); + self.dispatch('live'); + self.flip(); + }; + // as window.URL.createObjectURL() is deprecated, adding a check so that it works in Safari. + // older browsers may not have srcObject + if ("srcObject" in video) { + video.srcObject = stream; + } + else { + // using URL.createObjectURL() as fallback for old browsers + video.src = window.URL.createObjectURL(stream); + } }) .catch( function(err) { - return self.dispatch('error', "Could not access webcam: " + err.name + ": " + err.message, err); + // JH 2016-07-31 Instead of dispatching error, now falling back to Flash if userMedia fails (thx @john2014) + // JH 2016-08-07 But only if flash is actually installed -- if not, dispatch error here and now. + if (self.params.enable_flash && self.detectFlash()) { + setTimeout( function() { self.params.force_flash = 1; self.attach(elem); }, 1 ); + } + else { + self.dispatch('error', err); + } }); } - else { + else if (this.iOS) { + // prepare HTML elements + var div = document.createElement('div'); + div.id = this.container.id+'-ios_div'; + div.className = 'webcamjs-ios-placeholder'; + div.style.width = '' + this.params.width + 'px'; + div.style.height = '' + this.params.height + 'px'; + div.style.textAlign = 'center'; + div.style.display = 'table-cell'; + div.style.verticalAlign = 'middle'; + div.style.backgroundRepeat = 'no-repeat'; + div.style.backgroundSize = 'contain'; + div.style.backgroundPosition = 'center'; + var span = document.createElement('span'); + span.className = 'webcamjs-ios-text'; + span.innerHTML = this.params.iosPlaceholderText; + div.appendChild(span); + var img = document.createElement('img'); + img.id = this.container.id+'-ios_img'; + img.style.width = '' + this.params.dest_width + 'px'; + img.style.height = '' + this.params.dest_height + 'px'; + img.style.display = 'none'; + div.appendChild(img); + var input = document.createElement('input'); + input.id = this.container.id+'-ios_input'; + input.setAttribute('type', 'file'); + input.setAttribute('accept', 'image/*'); + input.setAttribute('capture', 'camera'); + + var self = this; + var params = this.params; + // add input listener to load the selected image + input.addEventListener('change', function(event) { + if (event.target.files.length > 0 && event.target.files[0].type.indexOf('image/') == 0) { + var objURL = URL.createObjectURL(event.target.files[0]); + + // load image with auto scale and crop + var image = new Image(); + image.addEventListener('load', function(event) { + var canvas = document.createElement('canvas'); + canvas.width = params.dest_width; + canvas.height = params.dest_height; + var ctx = canvas.getContext('2d'); + + // crop and scale image for final size + ratio = Math.min(image.width / params.dest_width, image.height / params.dest_height); + var sw = params.dest_width * ratio; + var sh = params.dest_height * ratio; + var sx = (image.width - sw) / 2; + var sy = (image.height - sh) / 2; + ctx.drawImage(image, sx, sy, sw, sh, 0, 0, params.dest_width, params.dest_height); + + var dataURL = canvas.toDataURL(); + img.src = dataURL; + div.style.backgroundImage = "url('"+dataURL+"')"; + }, false); + + // read EXIF data + var fileReader = new FileReader(); + fileReader.addEventListener('load', function(e) { + var orientation = self.exifOrientation(e.target.result); + if (orientation > 1) { + // image need to rotate (see comments on fixOrientation method for more information) + // transform image and load to image object + self.fixOrientation(objURL, orientation, image); + } else { + // load image data to image object + image.src = objURL; + } + }, false); + + // Convert image data to blob format + var http = new XMLHttpRequest(); + http.open("GET", objURL, true); + http.responseType = "blob"; + http.onload = function(e) { + if (this.status == 200 || this.status === 0) { + fileReader.readAsArrayBuffer(this.response); + } + }; + http.send(); + + } + }, false); + input.style.display = 'none'; + elem.appendChild(input); + // make div clickable for open camera interface + div.addEventListener('click', function(event) { + if (params.user_callback) { + // global user_callback defined - create the snapshot + self.snap(params.user_callback, params.user_canvas); + } else { + // no global callback definied for snapshot, load image and wait for external snap method call + input.style.display = 'block'; + input.focus(); + input.click(); + input.style.display = 'none'; + } + }, false); + elem.appendChild(div); + this.loaded = true; + this.live = true; + } + else if (this.params.enable_flash && this.detectFlash()) { // flash fallback window.Webcam = Webcam; // needed for flash-to-js interface var div = document.createElement('div'); div.innerHTML = this.getSWFHTML(); elem.appendChild( div ); } + else { + this.dispatch('error', new WebcamError( this.params.noInterfaceFoundText )); + } // setup final crop for live preview if (this.params.crop_width && this.params.crop_height) { @@ -200,7 +492,13 @@ var Webcam = { delete this.stream; delete this.video; } - + + if ((this.userMedia !== true) && this.loaded && !this.iOS) { + // call for turn off camera in flash + var movie = this.getMovie(); + if (movie && movie._releaseCamera) movie._releaseCamera(); + } + if (this.container) { this.container.innerHTML = ''; delete this.container; @@ -271,16 +569,24 @@ var Webcam = { return true; } else if (name == 'error') { + var message; + if ((args[0] instanceof FlashError) || (args[0] instanceof WebcamError)) { + message = args[0].message; + } else { + message = "Could not access webcam: " + args[0].name + ": " + + args[0].message + " " + args[0].toString(); + } + // default error handler if no custom one specified - alert("Webcam.js Error: " + args[0]); + alert("Webcam.js Error: " + message); } return false; // no hook defined }, - - setSWFLocation: function(url) { - // set location of SWF movie (defaults to webcam.swf in cwd) - this.swfURL = url; + + setSWFLocation: function(value) { + // for backward compatibility. + this.set('swfURL', value); }, detectFlash: function() { @@ -315,22 +621,23 @@ var Webcam = { getSWFHTML: function() { // Return HTML for embedding flash based webcam capture movie - var html = ''; + var html = '', + swfURL = this.params.swfURL; // make sure we aren't running locally (flash doesn't work) if (location.protocol.match(/file/)) { - this.dispatch('error', "Flash does not work from local disk. Please run from a web server."); + this.dispatch('error', new FlashError("Flash does not work from local disk. Please run from a web server.")); return '

ERROR: the Webcam.js Flash fallback does not work from local disk. Please run it from a web server.

'; } // make sure we have flash if (!this.detectFlash()) { - this.dispatch('error', "Adobe Flash Player not found. Please install from get.adobe.com/flashplayer and try again."); - return '

ERROR: No Adobe Flash Player detected. Webcam.js relies on Flash for browsers that do not support getUserMedia (like yours).

'; + this.dispatch('error', new FlashError("Adobe Flash Player not found. Please install from get.adobe.com/flashplayer and try again.")); + return '

' + this.params.flashNotDetectedText + '

'; } // set default swfURL if not explicitly set - if (!this.swfURL) { + if (!swfURL) { // find our script tag, and use that base URL var base_url = ''; var scpts = document.getElementsByTagName('script'); @@ -341,8 +648,8 @@ var Webcam = { idx = len; } } - if (base_url) this.swfURL = base_url + '/webcam.swf'; - else this.swfURL = 'webcam.swf'; + if (base_url) swfURL = base_url + '/webcam.swf'; + else swfURL = 'webcam.swf'; } // if this is the user's first visit, set flashvar so flash privacy settings panel is shown first @@ -359,17 +666,17 @@ var Webcam = { } // construct object/embed tag - html += ''; + html += ''; return html; }, getMovie: function() { // get reference to movie object/embed in DOM - if (!this.loaded) return this.dispatch('error', "Flash Movie is not loaded yet"); + if (!this.loaded) return this.dispatch('error', new FlashError("Flash Movie is not loaded yet")); var movie = document.getElementById('webcam_movie_obj'); if (!movie || !movie._snap) movie = document.getElementById('webcam_movie_embed'); - if (!movie) this.dispatch('error', "Cannot locate Flash movie in DOM"); + if (!movie) this.dispatch('error', new FlashError("Cannot locate Flash movie in DOM")); return movie; }, @@ -496,17 +803,21 @@ var Webcam = { ); // remove preview - this.unfreeze(); + if (this.params.unfreeze_snap) this.unfreeze(); }, snap: function(user_callback, user_canvas) { + // use global callback and canvas if not defined as parameter + if (!user_callback) user_callback = this.params.user_callback; + if (!user_canvas) user_canvas = this.params.user_canvas; + // take snapshot and return image data uri var self = this; var params = this.params; - if (!this.loaded) return this.dispatch('error', "Webcam is not loaded yet"); - // if (!this.live) return this.dispatch('error', "Webcam is not live yet"); - if (!user_callback) return this.dispatch('error', "Please provide a callback function or canvas to snap()"); + if (!this.loaded) return this.dispatch('error', new WebcamError("Webcam is not loaded yet")); + // if (!this.live) return this.dispatch('error', new WebcamError("Webcam is not live yet")); + if (!user_callback) return this.dispatch('error', new WebcamError("Please provide a callback function or canvas to snap()")); // if we have an active preview freeze, use that if (this.preview_active) { @@ -578,6 +889,30 @@ var Webcam = { // fire callback right away func(); } + else if (this.iOS) { + var div = document.getElementById(this.container.id+'-ios_div'); + var img = document.getElementById(this.container.id+'-ios_img'); + var input = document.getElementById(this.container.id+'-ios_input'); + // function for handle snapshot event (call user_callback and reset the interface) + iFunc = function(event) { + func.call(img); + img.removeEventListener('load', iFunc); + div.style.backgroundImage = 'none'; + img.removeAttribute('src'); + input.value = null; + }; + if (!input.value) { + // No image selected yet, activate input field + img.addEventListener('load', iFunc); + input.style.display = 'block'; + input.focus(); + input.click(); + input.style.display = 'none'; + } else { + // Image already selected + iFunc(null); + } + } else { // flash fallback var raw_data = this.getMovie()._snap(); @@ -611,12 +946,11 @@ var Webcam = { // camera is live and ready to snap this.live = true; this.dispatch('live'); - this.flip(); break; case 'error': // Flash error - this.dispatch('error', msg); + this.dispatch('error', new FlashError(msg)); break; default: diff --git a/web_widget_image_webcam/static/src/js/webcam.swf b/web_widget_image_webcam/static/src/js/webcam.swf index 1e19c9deca0e36f63c97ed53aabbe8241e7763ea..e1d88cd2ea8e79ce81bfec8eecd6b03e7af2057a 100644 GIT binary patch literal 7090 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 literal 6944 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