ChatGPT解决这个技术问题 Extra ChatGPT

使用 HTML5/Canvas/JavaScript 在浏览器中截屏

Google 的“报告错误”或“反馈工具”可让您选择浏览器窗口的一个区域来创建屏幕截图,并提交您对错误的反馈。

https://i.stack.imgur.com/CDhEi.png

他们是怎么做到的? Google 的 JavaScript 反馈 API 从 here 加载,their overview of the feedback module 将演示屏幕截图功能。

Elliott Sprehn wrote in a Tweet 几天前:> @CatChen那个stackoverflow帖子不准确。 Google 反馈的屏幕截图完全是在客户端完成的。 :)
这合乎逻辑,因为他们想要准确地捕捉用户的浏览器如何呈现页面,而不是他们将如何使用他们的引擎在服务器端呈现它。如果您只将当前页面 DOM 发送到服务器,它将错过浏览器呈现 HTML 的任何不一致之处。这并不意味着陈的回答是错误的截图,只是看起来谷歌正在以不同的方式做这件事。
Elliott 今天提到了 Jan Kuča,我在 Jan 的推文中找到了这个链接:jankuca.tumblr.com/post/7391640769/…
稍后我将对此进行深入研究,看看如何使用客户端渲染引擎完成它,并检查 Google 是否真的这样做了。
我看到使用 compareDocumentPosition、getBoxObjectFor、toDataURL、drawImage、跟踪填充和类似的东西。它需要数千行混淆代码来去混淆和查看。我很想看到它的开源许可版本,我已经联系了 Elliott Sprehn!

P
Pang

JavaScript 可以读取 DOM 并使用 canvas 呈现相当准确的表示。我一直在研究将 HTML 转换为画布图像的脚本。今天决定将其实施为发送您所描述的反馈。

该脚本允许您创建反馈表单,其中包括在客户端浏览器上创建的屏幕截图以及表单。屏幕截图基于 DOM,因此可能不是 100% 准确到真实表示,因为它不会制作实际的屏幕截图,而是根据页面上可用的信息构建屏幕截图。

它不需要来自服务器的任何渲染,因为整个图像是在客户端的浏览器上创建的。 HTML2Canvas 脚本本身仍处于非常实验性的状态,因为它几乎无法解析我希望它解析的 CSS3 属性,即使代理可用,它也不支持加载 CORS 图像。

仍然相当有限的浏览器兼容性(不是因为无法支持更多,只是没有时间让它更多地支持跨浏览器)。

有关更多信息,请查看此处的示例:

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

edit html2canvas 脚本现在单独提供here 和一些examples here

edit 2 另一个确认 Google 使用了非常相似的方法(事实上,根据文档,唯一的主要区别是它们的异步遍历/绘图方法)可以在 Elliott Sprehn 的演示文稿中找到Google 反馈小组:http://www.elliottsprehn.com/preso/fluentconf/


非常酷,Sikuli 或 Selenium 可能适合访问不同的站点,将来自测试工具的站点快照与您的 html2canvas.js 渲染图像进行像素相似度比较!想知道您是否可以使用非常简单的公式求解器自动遍历 DOM 的各个部分,以找到如何为 getBoundingClientRect 不可用的浏览器解析备用数据源。如果它是开源的,我可能会使用它,正在考虑自己玩弄它。干得好尼克拉斯!
@Luke Stanley 我很可能会在本周末将源代码放到 github 上,在此之前我仍然想做一些小的清理和更改,以及摆脱它目前拥有的不必要的 jQuery 依赖。
源代码现在可在 github.com/niklasvh/html2canvas 上找到,其中有一些正在使用的脚本示例 html2canvas.hertzen.com。仍然有很多错误需要修复,所以我不建议在实时环境中使用该脚本。
使其适用于 SVG 的任何解决方案都会有很大帮助。它不适用于 highcharts.com
@Niklas 我看到你的例子成长为一个真正的项目。也许更新您对项目的实验性质最赞成的评论。经过近 900 次提交后,我认为这不仅仅是一个实验 ;-)
M
Matt Sinclair

您的网络应用程序现在可以使用 getUserMedia() 对客户端的整个桌面进行“原生”屏幕截图:

看看这个例子:

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

客户端必须使用 chrome(目前),并且需要在 chrome://flags 下启用屏幕捕获支持。


我找不到任何只截屏的演示——一切都是关于屏幕共享的。将不得不尝试它。
@XMight,您可以通过切换屏幕捕获支持标志来选择是否允许这样做。
@XMight 请不要这样想。 Web 浏览器应该能够做很多事情,但不幸的是它们与它们的实现不一致。如果浏览器有这样的功能,那绝对没问题,只要询问用户。没有您的注意,任何人都无法制作屏幕截图。但是太多的恐惧会导致糟糕的实现,比如剪贴板 API,它已经被完全禁用,而是创建确认对话框,比如网络摄像头、麦克风、屏幕截图功能等。
这已被弃用,并将根据 developer.mozilla.org/en-US/docs/Web/API/Navigator/getUserMedia 从标准中删除
@AgustinCautin Navigator.getUserMedia() 已被弃用,但在其下方显示“...请使用较新的 navigator.mediaDevices.getUserMedia()”,即它只是被较新的 API 取代。
K
Kamil Kiełczewski

概念验证

作为 Niklas mentioned,您可以使用 html2canvas 库在浏览器中使用 JS 截取屏幕截图。在这一点上,我将通过提供一个使用此库截取屏幕截图的示例(“概念证明”)来扩展他的答案:

函数报告(){让区域=文档.querySelector(“body”); // 全屏 html2canvas(region, { onrendered: function(canvas) { let pngUrl = canvas.toDataURL(); // dataURL 格式的 png let img = document.querySelector(".screen"); img.src = pngUrl; // 在这里你可以允许用户设置 bug-region // 并使用 'pngUrl' 发送到服务器 }, }); } .container { 边距顶部:10px;边框:纯色 1px 黑色; }

屏幕截图测试器

onrendered 中的 report() 函数中,将图像作为数据 URI 后,您可以将其显示给用户,并允许他通过鼠标绘制“bug 区域”,然后将屏幕截图和区域坐标发送到服务器。

this example 中制作了 async/await 版本:具有很好的 makeScreenshot() 功能.

更新

简单示例,可让您截屏、选择区域、描述错误和发送 POST 请求 (here jsfiddle)(主要功能是 report())。

异步函数报告(){让截图=等待makeScreenshot(); // png dataUrl let img = q(".screen"); img.src = 截图;让 c = q(".bug-container"); c.classList.remove('hide') let box = await getBox(); c.classList.add('隐藏');发送(截图,框); // 带有错误图像、区域和描述的 sed 发布请求 alert('要查看带有图像的 POST 请求,请转到:chrome 控制台 > 网络选项卡'); } // ----- 辅助函数 let q = s => document.querySelector(s); // 查询选择器助手 window.report = report; // 绑定报告在小提琴中可见 html async function makeScreenshot(selector="body") { return new Promise((resolve, reject) => { let node = document.querySelector(selector); html2canvas(node, { onrendered: ( canvas) => { 让 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 "); 让 send = q(".send"); 让 start=0; 让 sx,sy,ex,ey=-1; r.style.width=0; r.style.height=0; 让 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 //缩小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();让 req = new XMLHttpRequest(); formData.append("box", JSON.stringify(box)); formData.append("截图", image); req.open("POST", '/upload/screenshot'); req.send(formData); } .bug-container { 背景:rgb(255,0,0,0.1);边距顶部:20px;文本对齐:居中; } .send { 边框半径:5px;填充:10px;背景:绿色;光标:指针; } .region { 位置:绝对;背景:rgba(255,0,0,0.4); } .example { 高度:100px;背景:黄色; } .bug { 边距顶部:10px;光标:十字准线; } .hide { 显示:无; } .screen { 指针事件:无 } < div>Screenshot tester

Lorem ipsum
< div>选择bug区域:单击一次-移动鼠标-再次单击
发送错误


我认为您被否决的原因很可能是 html2canvas 库是他的库,而不是他简单指出的工具。
如果您不想捕捉后期处理效果(作为模糊滤镜),那很好。
限制 脚本使用的所有图像都需要位于同一来源下,才能在没有代理帮助的情况下读取它们。同样,如果页面上有其他画布元素,这些元素已经被跨域内容污染,它们将变脏并且不再被 html2canvas 读取。
N
Nikolay Makhonin

使用 getDisplayMedia API 以 Canvas 或 Jpeg Blob / ArrayBuffer 形式获取屏幕截图:

FIX 1:仅将 getUserMedia 与 chromeMediaSource 用于 Electron.js
FIX 2:抛出错误,而不是返回 null 对象
FIX 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

想知道为什么这只有 1 票,事实证明这真的很有帮助!
请问它是如何工作的?能给像我这样的新手提供demo吗?谢谢
@kabrice 我添加了一个演示。只需将代码放在 Chrome 控制台中即可。如果您需要旧浏览器支持,请使用:babeljs.io/en/repl
这在 Firefox 中拍了一张 mysef 的照片......只是不要允许使用 cam :)
我喜欢这个答案,但不幸的是,它在屏幕截图中包含了“选择要共享的屏幕”对话框,并且覆盖了屏幕的很大一部分。至少对我来说使用 Chrome。
B
BobbyTables

这是一个完整的屏幕截图示例,适用于 2021 年的 chrome。最终结果是一个可以传输的 blob。流程为:请求媒体>抓帧>画到画布>转移到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));

J
JSON C11

下面是一个使用示例: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 文档也值得一试。


a
amiad

你可以试试我的新 JS 库:screenshot.js

它可以拍摄真实的截图。

您加载脚本:

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

并截图:

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

您可以在项目页面中阅读更多选项。


他问他们是怎么做到的。没有图书馆可以做到这一点。