实现一个在 Electron 内截屏的功能,大概有六种实现方式,讨论一下选择哪个方案。最后再说一下高分屏的截图如何变得清晰。

可选方案

第一种,使用 Canvas 截图

这种技术有一个非常有名的库:html2canvas,可以支持相对比较简单的 DOM 截图

目前一直是处于实验状态(very experimental state),如果理解这个库的限制,那么是一个还不错的选择

原理是使用 BoundCurves 对 DOM 进行 path 绘制。例如下面一段选自 html2canvas 库中的代码:

this.topLeftPaddingBox =
    tlh > 0 || tlv > 0
        ? getCurvePoints(
                bounds.left + borderLeftWidth,
                bounds.top + borderTopWidth,
                Math.max(0, tlh - borderLeftWidth),
                Math.max(0, tlv - borderTopWidth),
                CORNER.TOP_LEFT
            )
        : new Vector(bounds.left + borderLeftWidth, bounds.top + borderTopWidth);

计算每一个 DOM 的形状,再把这些数据,比如 topLeftPaddingBox 放到 Stack 中,最后放在队列中,一个一个的绘制。

优势

  • 当然是兼容性非常好, 支持 canvas 的就行

劣势

  • 因为是要把现存的 DOM 搬到 canvas 中,所以只能识别库支持的 css 属性,比如 transform 不支持。
  • 获取资源要同源,如果有其它跨域资源,布局会混乱
  • 大量计算工作,性能损耗较大

第二种,使用 SVG 构图

如下简单的例子,可以把 DOM 通过 svg 的 foreignObject 放到 canvas 中,就可以截图了。

// 这段代码出自它处

const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
const data = '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">' +
        '<foreignObject width="100%" height="100%">' +
        '<div xmlns="http://www.w3.org/1999/xhtml" style="font-size:40px">' +
            '<em>I</em> like' + 
            '<span style="color:white; text-shadow:0 0 2px blue;">' +
            'cheese</span>' +
        '</div>' +
        '</foreignObject>' +
        '</svg>'
const DOMURL = window.URL || window.webkitURL || window
const img = new Image()
const svg = new Blob([data], {type: 'image/svg+xml;charset=utf-8'})
const url = DOMURL.createObjectURL(svg)
img.onload = function () {
    ctx.drawImage(img, 0, 0)
    DOMURL.revokeObjectURL(url)
}
img.src = url

做得好的是 rasterizeHTML.js,同时可以支持直接填写url,然后获取后自己截图。

rasterizeHTML.js 和 html2canvas 的区别
之前是 rasterizeHTML.js 使用 foreignObject,而 html2canvas 自己从最基础的开始。
推荐直接使用 foreignObject 更加安全,目前 html2canvas 和 rasterizeHTML 都兼容使用 foreignObject (html2canvas 看源代码里面已经支持了)。

第三种,使用 Chrome 的 getDisplayMedia

const captureStream = await navigator.mediaDevices.getDisplayMedia({
    audio: true,
    video: {
        width: { max: 1280 },
        height: { max: 720 }
    }
})

比如在 Chrome 中执行,会弹出下面的窗口,然后开启屏幕分享:
Chrome getDisplayMedia

这种可以实现录制整个屏幕,或者某个 Tab,从而分享画面,不过录音这个功能会碰到一些阻碍。

优势

  • 实现非网页内的内容也可以截取

劣势

  • 需要用户手动允许系统的屏幕录像权限
  • 获得的视频需要截取

第四种,使用 Electron 的 webContents.capturePage

Electron 内置了一个截取网页内的接口 webContents.capturePage ,如下:

const remote = require('electron').remote
const webContents = remote.getCurrentWebContents()

const nativeImage = await webContents.capturePage({
    x: 0,
    y: 0,
    width: 1000,
    height: 2000
})
remote.require('fs').writeFile(TEMP_URL, nativeImage.toPng())

优势

  • 简单,快捷
  • 支持截取完整的页面

劣势

  • 只能截取当前页面的内容

第五种,使用 Electron DesktopCapturer module

五的第一种:

DesktopCapturer 模块里面可以通过设置录屏的 video,然后通过 canvas 获取录制屏幕数据。前面的 getDisplayMedia 也可以通过这个方式获取到截屏图片。

例如下面的代码:

const { desktopCapturer } = require('electron')

const sources = await desktopCapturer.getSources({
    types: ['window', 'screen']
})

for (let i = 0; i < sources.length; ++i) {
    if (sources[i].name === 'Electron') {
        navigator.mediaDevices.getUserMedia({
            audio: false,
            video: {
                mandatory: {
                    chromeMediaSource: 'desktop',
                    chromeMediaSourceId: sources[i].id,
                    minWidth: 1280,
                    maxWidth: 1280,
                    minHeight: 720,
                    maxHeight: 720
                }
            }
        })
            .then((stream) => handleStream(stream))
            .catch((e) => handleError(e))
        return
    }
}

function handleStream (stream) {
    const video = document.querySelector('video')
    video.srcObject = stream
    video.onloadedmetadata = (e) => video.play()
}

function handleError (e) {
    console.log(e)
}

五的第二种:

之前看到有个地方,用 Thumbnail 拿来做屏幕截图 :P

const { desktopCapturer } = require('electron')

const sources = await desktopCapturer.getSources({
    types: ['window', 'screen'],
}, thumbnailSizeSize: {
    width: 300,
    height: 300,
})

const source = sources.find(e => e.name === 'Electron Window Title')
const thumbnailNativeImage = source.thumbnail

优势

  • 可以搜索特定窗口进行录屏
  • electron 封装了一层,使用更加简便

劣势

  • 如同前面的 getDisplayMedia,也需要用户手动开启屏幕录像权限
  • 如果用 desktopCapturer 的 thumbnail 来截屏,耗性能

第六种,使用系统原生的屏幕录制接口

这种就不细讲了,每端都适配复杂的原生接口,这样就已经迷失方向了。

截屏的图片不是 2x 图片

如果用上面的任何一种方法,在高分屏里面截图(window.devicePixelRatio === 2),最后得到的图片还是图片宽高放大一倍,但像素还是低一倍。

后来发现其实是图片头中有记录 meta 信息。里面就有关于 dpi 的信息。找到包 changedpi 可以修改 canvas 和 DataUrl 拿到的图片 dpi。

// 创建一个标准的 72 dpi
const IMAGE_QUALITY = 0.92
const dataUrl = canvas.toDataURL('image/jpeg', IMAGE_QUALITY);

function maxDPIRatio() {
  const displayScale = remote.screen.getAllDisplays().map(display => display.scaleFactor)
  return Math.max.apply(null, displayScale) // 高分屏里面一般是 2
}

const STANDARD_DPI = 72
const daurl150dpi = changedpi.changeDpiDataUrl(dataUrl, maxDPIRatio() * STANDARD_DPI);

canvas 一般情况下,最高只能获得 72 dpi 分辨率的图片。

总结

每个点都没有太详细的讲,但已经可以通过这些选择最终的方案了。最后当然推荐使用的是 webContents.capturePage 的方法。简单易用,在功能上能做到最好。

参考

  • https://stackoverflow.com/questions/16870404/whats-the-difference-between-html2canvas-and-rasterizehtml-js
  • https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia
  • https://ourcodeworld.com/articles/read/280/creating-screenshots-of-your-app-or-the-screen-in-electron-framework)

引用

  • https://developer.mozilla.org/zh-CN/docs/Web/SVG/Element/foreignObject
  • https://www.electronjs.org/docs/api/web-contents#contentscapturepagerect-callback
  • https://github.com/electron/electron/issues/7387
  • https://github.com/shutterstock/changeDPI