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 1e19c9de..e1d88cd2 100644 Binary files a/web_widget_image_webcam/static/src/js/webcam.swf and b/web_widget_image_webcam/static/src/js/webcam.swf differ