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:

  1. Use WebRTC’s getUserMedia to start the camera preview in a video element and capture a frame with a canvas 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.
  2. 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

  1. 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
      }
    }
    
  2. 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))
      }
    }
    
  3. 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);
      }
    };
    
  4. 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 an img 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")
    }
    
  5. 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;
      }
      //...
    }
    
  6. 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;
    });
    
  7. 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

  1. 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();
    });
    
  2. 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.