Usando HTML5/Canvas/JavaScript para tirar screenshots in-browser

Google's "Relatar um Bug" ou "Ferramenta de Feedback" permite que você selecione uma área da janela do seu navegador para criar uma captura de tela que é enviada com o seu feedback sobre um bug.

Screenshot by Jason Small, posted in a duplicate question.

Como estão eles a fazer isto? A API de feedback JavaScript do Google's é carregada a partir de aqui e sua visão geral do módulo de feedback irá demonstrar a capacidade de captura de tela.

Solução

O JavaScript pode ler o DOM e fazer uma representação bastante precisa do mesmo utilizando a canvas. Eu tenho trabalhado em um script que converte HTML em uma imagem em tela. Decidi hoje fazer uma implementação dele para enviar feedbacks, como você descreveu.

O script permite criar formulários de feedback que incluem uma captura de tela, criada no navegador do cliente, junto com o formulário. A captura de tela é baseada no DOM e, como tal, pode não ser 100% precisa para a representação real, pois não faz uma captura de tela real, mas constrói a captura de tela com base nas informações disponíveis na página.

Ele não requer qualquer renderização do servidor, pois a imagem inteira é criada no navegador do cliente. O script HTML2Canvas em si ainda está em um estado muito experimental, pois não analisa quase tanto os atributos CSS3 que eu gostaria que fosse, nem tem qualquer suporte para carregar imagens CORS mesmo que um proxy estivesse disponível.

Ainda é bastante limitada a compatibilidade do navegador (não porque não foi possível suportar mais, apenas porque não houve tempo para torná-lo mais compatível com o cross browser).

Para mais informações, dê uma olhada nos exemplos aqui:

http://hertzen.com/experiments/jsfeedback/

**edit*** O script html2canvas está agora disponível separadamente aqui e alguns exemplos aqui.

**edit 2*** Outra confirmação de que o Google utiliza um método muito semelhante (de facto, com base na documentação, a única grande diferença é o seu método assimétrico de deslocação/desenho) pode ser encontrada nesta apresentação por Elliott Sprehn da equipa do Google Feedback: http://www.elliottsprehn.com/preso/fluentconf/

Comentários (20)

Seu aplicativo web pode agora tirar uma imagem 'nativa' de toda a área de trabalho do cliente utilizando getUserMedia():

Dê uma olhada neste exemplo:

https://www.webrtc-experiment.com/Pluginfree-Screen-Sharing/

O cliente terá que estar usando cromo (por enquanto) e terá que habilitar o suporte de captura de tela sob cromo:// bandeiras.

Comentários (8)

Como [Niklas mencionou][1] você pode usar a biblioteca [html2canvas][2] para fazer uma captura de tela usando o JS no navegador. Vou alargar a sua resposta neste ponto, fornecendo um exemplo de como tirar uma imagem de ecrã utilizando esta biblioteca:

function report() {
  let region = document.querySelector("body"); // whole screen
  html2canvas(region, {
    onrendered: function(canvas) {
      let pngUrl = canvas.toDataURL(); // png in dataURL format
      let img = document.querySelector(".screen");
      img.src = pngUrl; 

      // here you can allow user to set bug-region
      // and send it with 'pngUrl' to server
    },
  });
}
.container {
  margin-top: 10px;
  border: solid 1px black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.4.1/html2canvas.min.js"></script>
<div>Screenshot tester</div>
Take screenshot

<div class="container">

</div>

Em report() function in onrendered depois de obter imagem como URI de dados você pode mostrá-la ao usuário e permitir que ele desenhe a "região do bug" pelo mouse e depois envie uma captura de tela e coordenadas da região para o servidor.

Em [este exemplo][3] async/await versão foi feita: com a bela makeScreenshot() function[.][4]

ATUALIZAÇÃO

Exemplo simples que lhe permite tirar screenshot, selecionar região, descrever bug e enviar pedido POST ([aqui jsfiddle][5]) (a função principal é report()).

async function report() {
    let screenshot = await makeScreenshot(); // png dataUrl
    let img = q(".screen");
    img.src = screenshot; 

    let c = q(".bug-container");
    c.classList.remove('hide')

    let box = await getBox();    
    c.classList.add('hide');

    send(screenshot,box); // sed post request  with bug image, region and description
    alert('To see POST requset with image go to: chrome console > network tab');
}

// ----- Helper functions

let q = s => document.querySelector(s); // query selector helper
window.report = report; // bind report be visible in fiddle html

async function  makeScreenshot(selector="body") 
{
  return new Promise((resolve, reject) => {  
    let node = document.querySelector(selector);

    html2canvas(node, { onrendered: (canvas) => {
        let pngUrl = canvas.toDataURL();      
        resolve(pngUrl);
    }});  
  });
}

async function getBox(box) {
  return new Promise((resolve, reject) => {
     let b = q(".bug");
     let r = q(".region");
     let scr = q(".screen");
     let send = q(".send");
     let start=0;
     let sx,sy,ex,ey=-1;
     r.style.width=0;
     r.style.height=0;

     let drawBox= () => {
         r.style.left   = (ex > 0 ? sx : sx+ex ) +'px'; 
         r.style.top    = (ey > 0 ? sy : sy+ey) +'px';
         r.style.width  = Math.abs(ex) +'px';
         r.style.height = Math.abs(ey) +'px'; 
     }

     //console.log({b,r, scr});
     b.addEventListener("click", e=>{
       if(start==0) {
         sx=e.pageX;
         sy=e.pageY;
         ex=0;
         ey=0;
         drawBox();
       }
       start=(start+1)%3;       
     });

     b.addEventListener("mousemove", e=>{
       //console.log(e)
       if(start==1) {
           ex=e.pageX-sx;
           ey=e.pageY-sy
           drawBox(); 
       }
     });

     send.addEventListener("click", e=>{
       start=0;
       let a=100/75 //zoom out img 75%       
       resolve({
          x:Math.floor(((ex > 0 ? sx : sx+ex )-scr.offsetLeft)*a),
          y:Math.floor(((ey > 0 ? sy : sy+ey )-b.offsetTop)*a),
          width:Math.floor(Math.abs(ex)*a),
          height:Math.floor(Math.abs(ex)*a),
          desc: q('.bug-desc').value
          });

     });
  });
}

function send(image,box) {

    let formData = new FormData();
    let req = new XMLHttpRequest();

    formData.append("box", JSON.stringify(box)); 
    formData.append("screenshot", image);     

    req.open("POST", '/upload/screenshot');
    req.send(formData);
}
.bug-container { background: rgb(255,0,0,0.1); margin-top:20px; text-align: center; }
.send { border-radius:5px; padding:10px; background: green; cursor: pointer; }
.region { position: absolute; background: rgba(255,0,0,0.4); }
.example { height: 100px; background: yellow; }
.bug { margin-top: 10px; cursor: crosshair; }
.hide { display: none; }

<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.4.1/html2canvas.min.js"></script>

<div>Screenshot tester</div>
Report bug

<div class="example">Lorem ipsum</div>

<div class="bug-container hide">
  <div>Select bug region</div>
  <div class="bug">    

    <div class="region"></div> 
  </div>
  <div>
    <textarea class="bug-desc">Describe bug here...
Comentários (4)