Skip to content

Commit fca8e0d

Browse files
authored
Merge pull request #209 from zsalab/master
iOS failback support
2 parents c403199 + 0ea955b commit fca8e0d

File tree

4 files changed

+303
-4
lines changed

4 files changed

+303
-4
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Here are some live demos showcasing various features of the library:
4848
|------|-------|
4949
| [Basic Demo](https://pixlcore.com/demos/webcamjs/demos/basic.html) | Demonstrates a basic 320x240 image capture. |
5050
| [Large Demo](https://pixlcore.com/demos/webcamjs/demos/large.html) | Demonstrates capturing a large 640x480 image, but showing a live preview at 320x240. |
51+
| [Large iOS Demo](https://pixlcore.com/demos/webcamjs/demos/ios-large.html) | Demonstrates capturing a large 640x480 image on iOS platform. |
5152
| [Crop Demo](https://pixlcore.com/demos/webcamjs/demos/crop.html) | Demonstrates cropping a 240x240 square out of the center of a 320x240 webcam image. |
5253
| [Large Crop Demo](https://pixlcore.com/demos/webcamjs/demos/crop-large.html) | Demonstrates a large 480x480 square crop, from a 640x480 image capture, with a 240x240 live preview. |
5354
| [HD Demo](https://pixlcore.com/demos/webcamjs/demos/hd.html) | Demonstrates a 720p HD (1280x720) image capture (only supported by some webcams). |

demos/ios-large.html

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<!doctype html>
2+
3+
<html lang="en">
4+
<head>
5+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
6+
<title>WebcamJS Test Page - Large Capture</title>
7+
<style type="text/css">
8+
body { font-family: Helvetica, sans-serif; }
9+
h2, h3 { margin-top:0; }
10+
form { margin-top: 15px; }
11+
form > input { margin-right: 15px; }
12+
#results { float:right; margin:20px; padding:20px; border:1px solid; background:#ccc; }
13+
</style>
14+
</head>
15+
<body>
16+
<div id="results">Your captured image will appear here...</div>
17+
18+
<h1>WebcamJS Test Page - Large Capture</h1>
19+
<h3>Demonstrates 640x480 large capture with 320x240 small display</h3>
20+
21+
<div id="my_camera"></div>
22+
23+
<!-- First, include the Webcam.js JavaScript Library -->
24+
<script type="text/javascript" src="../webcam.min.js"></script>
25+
26+
<!-- Configure a few settings and attach camera -->
27+
<script language="JavaScript">
28+
Webcam.set({
29+
width: 320,
30+
height: 240,
31+
dest_width: 640,
32+
dest_height: 480,
33+
image_format: 'jpeg',
34+
jpeg_quality: 90,
35+
user_callback: function(data_uri) {
36+
// display results in page
37+
document.getElementById('results').innerHTML =
38+
'<h2>Here is your large image:</h2>' +
39+
'<img src="'+data_uri+'"/>';
40+
}
41+
});
42+
Webcam.attach( '#my_camera' );
43+
</script>
44+
</body>
45+
</html>

webcam.js

+256-3
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ var Webcam = {
4141
loaded: false, // true when webcam movie finishes loading
4242
live: false, // true when webcam is initialized and ready to snap
4343
userMedia: true, // true when getUserMedia is supported natively
44-
44+
45+
iOS: /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream,
46+
4547
params: {
4648
width: 0,
4749
height: 0,
@@ -58,7 +60,10 @@ var Webcam = {
5860
swfURL: '', // URI to webcam.swf movie (defaults to the js location)
5961
flashNotDetectedText: 'ERROR: No Adobe Flash Player detected. Webcam.js relies on Flash for browsers that do not support getUserMedia (like yours).',
6062
noInterfaceFoundText: 'No supported webcam interface found.',
61-
unfreeze_snap: true // Whether to unfreeze the camera after snap (defaults to true)
63+
unfreeze_snap: true, // Whether to unfreeze the camera after snap (defaults to true)
64+
iosPlaceholderText: 'Click here to open camera.',
65+
user_callback: null, // callback function for snapshot (used if no user_callback parameter given to snap function)
66+
user_canvas: null // user provided canvas for snapshot (used if no user_canvas parameter given to snap function)
6267
},
6368

6469
errors: {
@@ -100,6 +105,123 @@ var Webcam = {
100105
}
101106
},
102107

108+
exifOrientation: function(binFile) {
109+
// extract orientation information from the image provided by iOS
110+
// algorithm based on exif-js
111+
var dataView = new DataView(binFile);
112+
if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) {
113+
console.log('Not a valid JPEG file');
114+
return 0;
115+
}
116+
var offset = 2;
117+
var marker = null;
118+
while (offset < binFile.byteLength) {
119+
// find 0xFFE1 (225 marker)
120+
if (dataView.getUint8(offset) != 0xFF) {
121+
console.log('Not a valid marker at offset ' + offset + ', found: ' + dataView.getUint8(offset));
122+
return 0;
123+
}
124+
marker = dataView.getUint8(offset + 1);
125+
if (marker == 225) {
126+
offset += 4;
127+
var str = "";
128+
for (n = 0; n < 4; n++) {
129+
str += String.fromCharCode(dataView.getUint8(offset+n));
130+
}
131+
if (str != 'Exif') {
132+
console.log('Not valid EXIF data found');
133+
return 0;
134+
}
135+
136+
offset += 6; // tiffOffset
137+
var bigEnd = null;
138+
139+
// test for TIFF validity and endianness
140+
if (dataView.getUint16(offset) == 0x4949) {
141+
bigEnd = false;
142+
} else if (dataView.getUint16(offset) == 0x4D4D) {
143+
bigEnd = true;
144+
} else {
145+
console.log("Not valid TIFF data! (no 0x4949 or 0x4D4D)");
146+
return 0;
147+
}
148+
149+
if (dataView.getUint16(offset+2, !bigEnd) != 0x002A) {
150+
console.log("Not valid TIFF data! (no 0x002A)");
151+
return 0;
152+
}
153+
154+
var firstIFDOffset = dataView.getUint32(offset+4, !bigEnd);
155+
if (firstIFDOffset < 0x00000008) {
156+
console.log("Not valid TIFF data! (First offset less than 8)", dataView.getUint32(offset+4, !bigEnd));
157+
return 0;
158+
}
159+
160+
// extract orientation data
161+
var dataStart = offset + firstIFDOffset;
162+
var entries = dataView.getUint16(dataStart, !bigEnd);
163+
for (var i=0; i<entries; i++) {
164+
var entryOffset = dataStart + i*12 + 2;
165+
if (dataView.getUint16(entryOffset, !bigEnd) == 0x0112) {
166+
var valueType = dataView.getUint16(entryOffset+2, !bigEnd);
167+
var numValues = dataView.getUint32(entryOffset+4, !bigEnd);
168+
if (valueType != 3 && numValues != 1) {
169+
console.log('Invalid EXIF orientation value type ('+valueType+') or count ('+numValues+')');
170+
return 0;
171+
}
172+
var value = dataView.getUint16(entryOffset + 8, !bigEnd);
173+
if (value < 1 || value > 8) {
174+
console.log('Invalid EXIF orientation value ('+value+')');
175+
return 0;
176+
}
177+
return value;
178+
}
179+
}
180+
} else {
181+
offset += 2+dataView.getUint16(offset+2);
182+
}
183+
}
184+
return 0;
185+
},
186+
187+
fixOrientation: function(origObjURL, orientation, targetImg) {
188+
// fix image orientation based on exif orientation data
189+
// exif orientation information
190+
// http://www.impulseadventure.com/photo/exif-orientation.html
191+
// link source wikipedia (https://en.wikipedia.org/wiki/Exif#cite_note-20)
192+
var img = new Image();
193+
img.addEventListener('load', function(event) {
194+
var canvas = document.createElement('canvas');
195+
var ctx = canvas.getContext('2d');
196+
197+
// switch width height if orientation needed
198+
if (orientation < 5) {
199+
canvas.width = img.width;
200+
canvas.height = img.height;
201+
} else {
202+
canvas.width = img.height;
203+
canvas.height = img.width;
204+
}
205+
206+
// transform (rotate) image - see link at beginning this method
207+
switch (orientation) {
208+
case 2: ctx.transform(-1, 0, 0, 1, img.width, 0); break;
209+
case 3: ctx.transform(-1, 0, 0, -1, img.width, img.height); break;
210+
case 4: ctx.transform(1, 0, 0, -1, 0, img.height); break;
211+
case 5: ctx.transform(0, 1, 1, 0, 0, 0); break;
212+
case 6: ctx.transform(0, 1, -1, 0, img.height , 0); break;
213+
case 7: ctx.transform(0, -1, -1, 0, img.height, img.width); break;
214+
case 8: ctx.transform(0, -1, 1, 0, 0, img.width); break;
215+
}
216+
217+
ctx.drawImage(img, 0, 0);
218+
// pass rotated image data to the target image container
219+
targetImg.src = canvas.toDataURL();
220+
}, false);
221+
// start transformation by load event
222+
img.src = origObjURL;
223+
},
224+
103225
attach: function(elem) {
104226
// create webcam preview and attach to DOM element
105227
// pass in actual DOM reference, ID, or CSS selector
@@ -201,6 +323,109 @@ var Webcam = {
201323
}
202324
});
203325
}
326+
else if (this.iOS) {
327+
// prepare HTML elements
328+
var div = document.createElement('div');
329+
div.id = this.container.id+'-ios_div';
330+
div.className = 'webcamjs-ios-placeholder';
331+
div.style.width = '' + this.params.width + 'px';
332+
div.style.height = '' + this.params.height + 'px';
333+
div.style.textAlign = 'center';
334+
div.style.display = 'table-cell';
335+
div.style.verticalAlign = 'middle';
336+
div.style.backgroundRepeat = 'no-repeat';
337+
div.style.backgroundSize = 'contain';
338+
div.style.backgroundPosition = 'center';
339+
var span = document.createElement('span');
340+
span.className = 'webcamjs-ios-text';
341+
span.innerHTML = this.params.iosPlaceholderText;
342+
div.appendChild(span);
343+
var img = document.createElement('img');
344+
img.id = this.container.id+'-ios_img';
345+
img.style.width = '' + this.params.dest_width + 'px';
346+
img.style.height = '' + this.params.dest_height + 'px';
347+
img.style.display = 'none';
348+
div.appendChild(img);
349+
var input = document.createElement('input');
350+
input.id = this.container.id+'-ios_input';
351+
input.setAttribute('type', 'file');
352+
input.setAttribute('accept', 'image/*');
353+
input.setAttribute('capture', 'camera');
354+
355+
var self = this;
356+
var params = this.params;
357+
// add input listener to load the selected image
358+
input.addEventListener('change', function(event) {
359+
if (event.target.files.length > 0 && event.target.files[0].type.indexOf('image/') == 0) {
360+
var objURL = URL.createObjectURL(event.target.files[0]);
361+
362+
// load image with auto scale and crop
363+
var image = new Image();
364+
image.addEventListener('load', function(event) {
365+
var canvas = document.createElement('canvas');
366+
canvas.width = params.dest_width;
367+
canvas.height = params.dest_height;
368+
var ctx = canvas.getContext('2d');
369+
370+
// crop and scale image for final size
371+
ratio = Math.min(image.width / params.dest_width, image.height / params.dest_height);
372+
var sw = params.dest_width * ratio;
373+
var sh = params.dest_height * ratio;
374+
var sx = (image.width - sw) / 2;
375+
var sy = (image.height - sh) / 2;
376+
ctx.drawImage(image, sx, sy, sw, sh, 0, 0, params.dest_width, params.dest_height);
377+
378+
var dataURL = canvas.toDataURL();
379+
img.src = dataURL;
380+
div.style.backgroundImage = "url('"+dataURL+"')";
381+
}, false);
382+
383+
// read EXIF data
384+
var fileReader = new FileReader();
385+
fileReader.addEventListener('load', function(e) {
386+
var orientation = self.exifOrientation(e.target.result);
387+
if (orientation > 1) {
388+
// image need to rotate (see comments on fixOrientation method for more information)
389+
// transform image and load to image object
390+
self.fixOrientation(objURL, orientation, image);
391+
} else {
392+
// load image data to image object
393+
image.src = objURL;
394+
}
395+
}, false);
396+
397+
// Convert image data to blob format
398+
var http = new XMLHttpRequest();
399+
http.open("GET", objURL, true);
400+
http.responseType = "blob";
401+
http.onload = function(e) {
402+
if (this.status == 200 || this.status === 0) {
403+
fileReader.readAsArrayBuffer(this.response);
404+
}
405+
};
406+
http.send();
407+
408+
}
409+
}, false);
410+
input.style.display = 'none';
411+
elem.appendChild(input);
412+
// make div clickable for open camera interface
413+
div.addEventListener('click', function(event) {
414+
if (params.user_callback) {
415+
// global user_callback defined - create the snapshot
416+
self.snap(params.user_callback, params.user_canvas);
417+
} else {
418+
// no global callback definied for snapshot, load image and wait for external snap method call
419+
input.style.display = 'block';
420+
input.focus();
421+
input.click();
422+
input.style.display = 'none';
423+
}
424+
}, false);
425+
elem.appendChild(div);
426+
this.loaded = true;
427+
this.live = true;
428+
}
204429
else if (this.params.enable_flash && this.detectFlash()) {
205430
// flash fallback
206431
window.Webcam = Webcam; // needed for flash-to-js interface
@@ -254,7 +479,7 @@ var Webcam = {
254479
delete this.video;
255480
}
256481

257-
if ((this.userMedia !== true) && this.loaded) {
482+
if ((this.userMedia !== true) && this.loaded && !this.iOS) {
258483
// call for turn off camera in flash
259484
var movie = this.getMovie();
260485
if (movie && movie._releaseCamera) movie._releaseCamera();
@@ -567,6 +792,10 @@ var Webcam = {
567792
},
568793

569794
snap: function(user_callback, user_canvas) {
795+
// use global callback and canvas if not defined as parameter
796+
if (!user_callback) user_callback = this.params.user_callback;
797+
if (!user_canvas) user_canvas = this.params.user_canvas;
798+
570799
// take snapshot and return image data uri
571800
var self = this;
572801
var params = this.params;
@@ -645,6 +874,30 @@ var Webcam = {
645874
// fire callback right away
646875
func();
647876
}
877+
else if (this.iOS) {
878+
var div = document.getElementById(this.container.id+'-ios_div');
879+
var img = document.getElementById(this.container.id+'-ios_img');
880+
var input = document.getElementById(this.container.id+'-ios_input');
881+
// function for handle snapshot event (call user_callback and reset the interface)
882+
iFunc = function(event) {
883+
func.call(img);
884+
img.removeEventListener('load', iFunc);
885+
div.style.backgroundImage = 'none';
886+
img.removeAttribute('src');
887+
input.value = null;
888+
};
889+
if (!input.value) {
890+
// No image selected yet, activate input field
891+
img.addEventListener('load', iFunc);
892+
input.style.display = 'block';
893+
input.focus();
894+
input.click();
895+
input.style.display = 'none';
896+
} else {
897+
// Image already selected
898+
iFunc(null);
899+
}
900+
}
648901
else {
649902
// flash fallback
650903
var raw_data = this.getMovie()._snap();

0 commit comments

Comments
 (0)