How to Take a High-Resolution Photo in the Browser
As the web platform develops, we can build rich kinds of applications on it. We can build a web app like a barcode scanner or a document scanner using the getUserMedia
API to get camera frames for live analysis.
While a barcode scanner may not require high resolution, a document scanner often requires taking a high-resolution photo.
In this article, we are going to build a demo web app to illustrate ways to take a high-resolution photo in the browser. It mainly uses the following ways:
- Use WebRTC’s getUserMedia to start the camera preview in a
video
element and capture a frame with acanvas
element. There is also an Image Capture API which allows taking a photo that has a higher resolution than the camera preview’s. But the API has limited browser support. - Use an
input
element to call the HTML Media Capture API, i.e.<input type="file" name="image" accept="image/*" capture>
, which calls the system’s camera app to take a photo.
New HTML File
Create a new HTML file with the following content:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Take a High Resolution Photo</title>
<style>
.home {
display: flex;
align-items: center;
flex-direction: column;
}
</style>
</head>
<body>
<div class="home">
<h2>Take a Photo</h2>
</div>
<script>
</script>
</body>
</html>
Take a Photo using getUserMedia
-
Request camera permission when the page is loaded.
window.onload = async function() { await requestCameraPermission(); } async function requestCameraPermission() { const constraints = {video: true, audio: false}; const stream = await navigator.mediaDevices.getUserMedia(constraints); const tracks = stream.getTracks(); for (let i=0;i<tracks.length;i++) { const track = tracks[i]; track.stop(); // stop the opened camera } }
-
List camera devices in a
select
element.HTML:
<label> Camera: <select id="cameraSelect"></select> </label>
JavaScript:
const cameraSelect = document.getElementById("cameraSelect"); window.onload = async function() { await loadCameraDevices(); loadCameraDevicesToSelect(); } async function loadCameraDevices(){ const constraints = {video: true, audio: false}; const stream = await navigator.mediaDevices.getUserMedia(constraints); const devices = await navigator.mediaDevices.enumerateDevices(); for (let i=0;i<devices.length;i++){ let device = devices[i]; if (device.kind == 'videoinput'){ // filter out audio devices cameraDevices.push(device); } } const tracks = stream.getTracks(); // stop the camera to avoid the NotReadableError for (let i=0;i<tracks.length;i++) { const track = tracks[i]; track.stop(); } } function loadCameraDevicesToSelect(){ for (let i=0;i<cameraDevices.length;i++){ let device = cameraDevices[i]; cameraSelect.appendChild(new Option(device.label,device.deviceId)) } }
-
Start the selected camera and display the camera preview in a
video
element.document.getElementById("startCameraBtn").addEventListener('click', (event) => { console.log("start camera"); let options = {}; if (cameraSelect.selectedIndex != -1) { options.deviceId = cameraSelect.selectedOptions[0].value; } play(options); }); function play(options) { stop(); // close before play video.style.display = "block"; let constraints = {}; if (options.deviceId){ constraints = { video: {deviceId: options.deviceId}, audio: false } }else{ constraints = { video: {width:1280, height:720,facingMode: { exact: "environment" }}, audio: false } } navigator.mediaDevices.getUserMedia(constraints).then(function(stream) { localStream = stream; // Attach local stream to video element video.srcObject = stream; }).catch(function(err) { console.error('getUserMediaError', err, err.stack); }); } function stop() { try{ if (localStream){ const tracks = localStream.getTracks(); for (let i=0;i<tracks.length;i++) { const track = tracks[i]; track.stop(); } } } catch (e){ alert(e.message); } };
-
Use a
canvas
element to capture a frame.Append a
canvas
element in the HTML and then use it to capture a camera frame and display it in animg
element.HTML:
<button id="takePhotoBtn">Take Photo</button> <video class="camera" muted autoplay="autoplay" playsinline="playsinline" webkit-playsinline></video> <br/> <canvas id="hiddenCanvas"></canvas> <img id="photoTaken" />
JavaScript:
document.getElementById("takePhotoBtn").addEventListener('click', async (event) => { let src; src = captureFrame(); document.getElementById("photoTaken").src = src; }); function captureFrame(){ let w = video.videoWidth; let h = video.videoHeight; canvas.width = w; canvas.height = h; let ctx = canvas.getContext('2d'); ctx.drawImage(video, 0, 0, w, h); return canvas.toDataURL("image/jpeg") }
-
Specify a high resolution so that the frame captured has a high quality.
We can define several common resolutions in a
select
and try to use the selected resolution.HTML:
<label> Desired Resolution: <select id="resolutionSelect"> <option value="640x480">640x480</option> <option value="1280x720">1280x720</option> <option value="1920x1080">1920x1080</option> <option value="3840x2160">3840x2160</option> </select> </label>
We need to specify the resolution in the constraints for
getUserMadia
.function play(options){ if (options.deviceId){ constraints = { video: {deviceId: options.deviceId}, audio: false } }else{ constraints = { video: {width:1280, height:720,facingMode: { exact: "environment" }}, audio: false } } if (resolutionSelect.selectedIndex != -1) { let width = parseInt(resolutionSelect.selectedOptions[0].value.split("x")[0]); let height = parseInt(resolutionSelect.selectedOptions[0].value.split("x")[1]); constraints["video"]["width"] = width; constraints["video"]["height"] = height; } //... }
-
Take a photo using the Image Capture API
If the browser supports the Image Capture API, we can use it to take a photo instead of capturing a frame with canvas.
document.getElementById("takePhotoBtn").addEventListener('click', async (event) => { let src; if ("ImageCapture" in window) { try { const track = localStream.getVideoTracks()[0]; let imageCapture = new ImageCapture(track); let blob = await imageCapture.takePhoto(); src = URL.createObjectURL(blob); }catch(e) { src = captureFrame(); } }else{ src = captureFrame(); } document.getElementById("photoTaken").src = src; });
-
Display the size of the image taken.
document.getElementById("photoTaken").onload = function(){ let img = document.getElementById("photoTaken"); document.getElementById("info").innerText = "Image Width: " + img.naturalWidth +"\nImage Height: " + img.naturalHeight; }
Take a Photo using input Element
-
Add an
input
element for selecting an image file.<button id="loadFileBtn"> Load File </button> <input type="file" id="file" onchange="loadImageFromFile();" accept=".jpg,.jpeg,.png,.bmp" />
The
input
element is hidden and triggered with a button.document.getElementById("loadFileBtn").addEventListener('click', async (event) => { document.getElementById("file").click(); });
-
When an image file is selected, load it into an
img
element.function loadImageFromFile(){ let fileInput = document.getElementById("file"); let files = fileInput.files; if (files.length == 0) { return; } let file = files[0]; fileReader = new FileReader(); fileReader.onload = function(e){ document.getElementById("photoTaken").src = e.target.result; }; fileReader.onerror = function () { console.warn('oops, something went wrong.'); }; fileReader.readAsDataURL(file); }
Which to Use
If you need to do live image processing before taking a photo, you have to use the getUserMedia
approach.
If you only need to take a high-resolution photo, you can use the input
element.
Source Code
Get the source code of the demo to have a try:
https://github.com/tony-xlh/WebRTC-High-Resolution-Photo
There is also a document scanner demo which can capture high-resolution photos: https://github.com/tony-xlh/ImageCapture-Document-Scanner. It uses Dynamsoft Document Normalizer to detect document boundaries and correct the document image.