programing

HTML5/Canvas/JavaScript를 사용하여 브라우저 내 스크린샷 촬영

goodcopy 2023. 1. 30. 22:26
반응형

HTML5/Canvas/JavaScript를 사용하여 브라우저 내 스크린샷 촬영

Google의 "버그 보고" 또는 "피드백 도구"를 사용하여 브라우저 창의 영역을 선택하여 버그에 대한 피드백과 함께 제출되는 스크린샷을 만들 수 있습니다.

Google 피드백 도구 스크린샷 제이슨 스몰의 스크린샷, 중복된 질문으로 게시되었습니다.

어떻게 한 거지?Google의 JavaScript 피드백 API는 여기에서 로드되며 피드백 모듈에 대한 개요는 스크린샷 기능을 보여줍니다.

JavaScript는 DOM을 읽을 수 있으며 이를 매우 정확하게 표현합니다.canvasHTML을 캔버스 이미지로 변환하는 스크립트를 작성하고 있습니다.오늘 당신이 설명한 대로 피드백을 보내기 위해 그것을 구현하기로 결정했습니다.

이 스크립트를 사용하여 클라이언트의 브라우저에서 작성된 스크린샷과 양식을 포함하는 피드백 양식을 작성할 수 있습니다.스크린샷은 DOM을 기반으로 하므로 실제 스크린샷을 만드는 것이 아니라 페이지에 있는 정보를 기반으로 스크린샷을 작성하기 때문에 실제 표현에 100% 정확하지는 않을 수 있습니다.

전체 이미지가 클라이언트의 브라우저에 작성되므로 서버에서 렌더링할 필요가 없습니다.HTML2Canvas 스크립트 자체는 아직 매우 실험적인 상태입니다.이는 원하는 CSS3 속성을 거의 해석하지 않고 프록시가 사용 가능하더라도 CORS 이미지를 로드할 수 없기 때문입니다.

아직 브라우저 호환성에는 한계가 있습니다(더 많은 브라우저가 지원되지 않아서가 아니라 크로스 브라우저가 더 많이 지원되도록 할 시간이 없었습니다).

상세한 것에 대하여는, 다음의 예를 참조해 주세요.

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

html2canvas 스크립트를 편집할 수 있게 되었습니다.여기서는 몇 가지 예를 제시하겠습니다.

편집 2 Google이 매우 유사한 방법을 사용한다는 또 다른 확인(실제로 문서에 따르면 주요 차이점은 이동/이동 비동기 방식뿐)은 Google Feedback 팀의 Elliott Sprehn이 발표한 프레젠테이션에서 찾을 수 있습니다.http://www.elliottsprehn.com/preso/fluentconf/

이제 웹 앱은 다음을 사용하여 클라이언트 데스크톱 전체의 '원어민' 스크린샷을 찍을 수 있습니다.getUserMedia():

다음 예를 참조하십시오.

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

클라이언트는 (현재로서는) 크롬을 사용해야 하며 chrome://flags에서 화면 캡처 지원을 활성화해야 합니다.

PoC

Niklas가 언급했듯이 브라우저에서 JS를 사용하여 스크린샷을 찍기 위해 html2canvas 라이브러리를 사용할 수 있습니다.이 시점에서, 이 라이브러리를 사용해 스크린샷을 촬영하는 예(「Proof of Concept」)를 제시해, 그 답변을 확대합니다.

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>
<button onclick="report()">Take screenshot</button>

<div class="container">
  <img width="75%" class="screen">
</div>

»report()onrendered이미지를 데이터 URI로 가져온 후 사용자에게 보여주고 마우스로 "버그 영역"을 그리도록 한 다음 스크린샷과 영역 좌표를 서버로 전송할 수 있습니다.

예에서는 async/await nice 전 nice nice nice nice nice nice 。makeScreenshot()기능합니다.

갱신하다

스크린샷을 찍고 지역을 선택하고 버그를 설명하고 POST 요청을 전송할 수 있는 간단한 예(여기서 jsfiddle)입니다(주요 기능은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; }
.screen { pointer-events: none }
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.4.1/html2canvas.min.js"></script>
<body>
<div>Screenshot tester</div>
<button onclick="report()">Report bug</button>

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

<div class="bug-container hide">
  <div>Select bug region: click once - move mouse - click again</div>
  <div class="bug">    
    <img width="75%" class="screen" >
    <div class="region"></div> 
  </div>
  <div>
    <textarea class="bug-desc">Describe bug here...</textarea>
  </div>
  <div class="send">SEND BUG</div>
</div>

</body>

getDisplayMedia API를 사용하여 캔버스 또는 JPEG Blob/ArrayBuffer로 스크린샷을 가져옵니다.

수정 1: getUserMedia와 ChromeMediaSource를 함께 사용하는 것은 Electron.js에만 해당됩니다.
FIX 2: 오류 발생 대신 null 개체를 반환합니다.
수정 3: 데모를 수정하여 오류를 방지합니다.getDisplayMedia must be called from a user gesture handler

// docs: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia
// see: https://www.webrtc-experiment.com/Pluginfree-Screen-Sharing/#20893521368186473
// see: https://github.com/muaz-khan/WebRTC-Experiment/blob/master/Pluginfree-Screen-Sharing/conference.js

function getDisplayMedia(options) {
    if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
        return navigator.mediaDevices.getDisplayMedia(options)
    }
    if (navigator.getDisplayMedia) {
        return navigator.getDisplayMedia(options)
    }
    if (navigator.webkitGetDisplayMedia) {
        return navigator.webkitGetDisplayMedia(options)
    }
    if (navigator.mozGetDisplayMedia) {
        return navigator.mozGetDisplayMedia(options)
    }
    throw new Error('getDisplayMedia is not defined')
}

function getUserMedia(options) {
    if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
        return navigator.mediaDevices.getUserMedia(options)
    }
    if (navigator.getUserMedia) {
        return navigator.getUserMedia(options)
    }
    if (navigator.webkitGetUserMedia) {
        return navigator.webkitGetUserMedia(options)
    }
    if (navigator.mozGetUserMedia) {
        return navigator.mozGetUserMedia(options)
    }
    throw new Error('getUserMedia is not defined')
}

async function takeScreenshotStream() {
    // see: https://developer.mozilla.org/en-US/docs/Web/API/Window/screen
    const width = screen.width * (window.devicePixelRatio || 1)
    const height = screen.height * (window.devicePixelRatio || 1)

    const errors = []
    let stream
    try {
        stream = await getDisplayMedia({
            audio: false,
            // see: https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints/video
            video: {
                width,
                height,
                frameRate: 1,
            },
        })
    } catch (ex) {
        errors.push(ex)
    }

    // for electron js
    if (navigator.userAgent.indexOf('Electron') >= 0) {
        try {
            stream = await getUserMedia({
                audio: false,
                video: {
                    mandatory: {
                        chromeMediaSource: 'desktop',
                        // chromeMediaSourceId: source.id,
                        minWidth         : width,
                        maxWidth         : width,
                        minHeight        : height,
                        maxHeight        : height,
                    },
                },
            })
        } catch (ex) {
            errors.push(ex)
        }
    }

    if (errors.length) {
        console.debug(...errors)
        if (!stream) {
            throw errors[errors.length - 1]
        }
    }

    return stream
}

async function takeScreenshotCanvas() {
    const stream = await takeScreenshotStream()

    // from: https://stackoverflow.com/a/57665309/5221762
    const video = document.createElement('video')
    const result = await new Promise((resolve, reject) => {
        video.onloadedmetadata = () => {
            video.play()
            video.pause()

            // from: https://github.com/kasprownik/electron-screencapture/blob/master/index.js
            const canvas = document.createElement('canvas')
            canvas.width = video.videoWidth
            canvas.height = video.videoHeight
            const context = canvas.getContext('2d')
            // see: https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement
            context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight)
            resolve(canvas)
        }
        video.srcObject = stream
    })

    stream.getTracks().forEach(function (track) {
        track.stop()
    })
    
    if (result == null) {
        throw new Error('Cannot take canvas screenshot')
    }

    return result
}

// from: https://stackoverflow.com/a/46182044/5221762
function getJpegBlob(canvas) {
    return new Promise((resolve, reject) => {
        // docs: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob
        canvas.toBlob(blob => resolve(blob), 'image/jpeg', 0.95)
    })
}

async function getJpegBytes(canvas) {
    const blob = await getJpegBlob(canvas)
    return new Promise((resolve, reject) => {
        const fileReader = new FileReader()

        fileReader.addEventListener('loadend', function () {
            if (this.error) {
                reject(this.error)
                return
            }
            resolve(this.result)
        })

        fileReader.readAsArrayBuffer(blob)
    })
}

async function takeScreenshotJpegBlob() {
    const canvas = await takeScreenshotCanvas()
    return getJpegBlob(canvas)
}

async function takeScreenshotJpegBytes() {
    const canvas = await takeScreenshotCanvas()
    return getJpegBytes(canvas)
}

function blobToCanvas(blob, maxWidth, maxHeight) {
    return new Promise((resolve, reject) => {
        const img = new Image()
        img.onload = function () {
            const canvas = document.createElement('canvas')
            const scale = Math.min(
                1,
                maxWidth ? maxWidth / img.width : 1,
                maxHeight ? maxHeight / img.height : 1,
            )
            canvas.width = img.width * scale
            canvas.height = img.height * scale
            const ctx = canvas.getContext('2d')
            ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height)
            resolve(canvas)
        }
        img.onerror = () => {
            reject(new Error('Error load blob to Image'))
        }
        img.src = URL.createObjectURL(blob)
    })
}

데모:

document.body.onclick = async () => {
    // take the screenshot
    var screenshotJpegBlob = await takeScreenshotJpegBlob()

    // show preview with max size 300 x 300 px
    var previewCanvas = await blobToCanvas(screenshotJpegBlob, 300, 300)
    previewCanvas.style.position = 'fixed'
    document.body.appendChild(previewCanvas)

    // send it to the server
    var formdata = new FormData()
    formdata.append("screenshot", screenshotJpegBlob)
    await fetch('https://your-web-site.com/', {
        method: 'POST',
        body: formdata,
        'Content-Type' : "multipart/form-data",
    })
}

// and click on the page

다음은 2021년에 크롬으로 작동하는 완전한 스크린샷 예입니다.최종 결과는 전송 준비가 된 블럽입니다.흐름은 request media > grab frame > draw to canvas > transfer to blob 입니다.메모리 효율을 높이고 싶은 경우 OffscreenCanvas 또는 ImageBitmapRenderingContext를 참조하십시오.

https://jsfiddle.net/v24hyd3q/1/

// Request media
navigator.mediaDevices.getDisplayMedia().then(stream => 
{
  // Grab frame from stream
  let track = stream.getVideoTracks()[0];
  let capture = new ImageCapture(track);
  capture.grabFrame().then(bitmap => 
  {
    // Stop sharing
    track.stop();
      
    // Draw the bitmap to canvas
    canvas.width = bitmap.width;
    canvas.height = bitmap.height;
    canvas.getContext('2d').drawImage(bitmap, 0, 0);
      
    // Grab blob from canvas
    canvas.toBlob(blob => {
        // Do things with blob here
        console.log('output blob:', blob);
    });
  });
})
.catch(e => console.log(e));

getDisplayMedia 를 사용한 예를 다음에 나타냅니다.

document.body.innerHTML = '<video style="width: 100%; height: 100%; border: 1px black solid;"/>';

navigator.mediaDevices.getDisplayMedia()
.then( mediaStream => {
  const video = document.querySelector('video');
  video.srcObject = mediaStream;
  video.onloadedmetadata = e => {
    video.play();
    video.pause();
  };
})
.catch( err => console.log(`${err.name}: ${err.message}`));

또한 Screen Capture API 문서도 확인할 수 있습니다.

새로운 JS 라이브러리 Screenshot.js를 사용해 보십시오.

실제 스크린샷을 찍을 수 있습니다.

스크립트를 로드합니다.

<script src="https://raw.githubusercontent.com/amiad/screenshot.js/master/screenshot.js"></script>

스크린샷을 찍습니다.

new Screenshot({success: img => {
        // callback function
        myimage = img;
    }});

프로젝트 페이지에서 더 많은 옵션을 읽을 수 있습니다.

언급URL : https://stackoverflow.com/questions/4912092/using-html5-canvas-javascript-to-take-in-browser-screenshots

반응형