Electron 复制和粘贴图片实现详解

最近,摸鱼派客户端增强了图片方面的功能,支持复制客户端上消息的图片和粘贴图片到消息框,之前消息框仅能支援图片文件的粘贴,而现在网页的图片和富文本的图片也都可以了。接下来将详细介绍这两个功能的具体实现:

粘贴图片

粘贴图片是几乎纯前端的功能,比如摸鱼派的编辑器就有实现,基本思路是监听 Paste 事件,然后对 clipboardData 解析处理。[1] 通常,会根据每个 clipboardData 的 item 的 type 处理,type 的值为 MIME 类型。那么,在这里我们需要处理两种情况,一种是 image/xxxx,另一种是 text/html

document.addEventListener('paste', async (ev) => {
    // 获取 Clipboard Data 的项
    let fileList = [];
    let items = ev.clipboardData && ev.clipboardData.items;
    for (var i = 0; items && i < items.length; i++) {
        const item = items[i];
        // 图片文件
        if (item.type.startsWith('image')) {
            fileList.push(items[i].getAsFile());
        }
        // 富文本复制会变成 `text/html`
        if (items[i].type == 'text/html') {
            // 下面解析
            let files = await htmlGetImg(items[i])
            files = files || []
            // 下面解析
            files = files.map(f => f.startsWith('file:') ? constructFileFromLocalFileData(new LocalFileData(f.replace(/file:\/\/\//g, ''))) : f)
            fileList = fileList.concat(files);
        }
    }
    if (file.length == 0) return;
    // 将图片文件上传并插入编辑框
    await this.uploadImg({ target: { files: file } });
});

如果复制的是图片文件,就比较简单。直接将图片剪贴板 item 转为图片 items[i].getAsFile() 然后上传即可。

而复杂的在于富文本的处理,因为他是一串 HTML 代码,所以就需要解析处理。这里我们编写了一个 htmlGetImg 的函数来处理:

htmlGetImg(item) {
     return new Promise((resolve) => {
          // 将剪贴板数据转为文本
          item.getAsString((html) => {
                // 使用正则解析出图片地址并返回
                resolve(html.match(/(?<=src=")([^"]+)/g))
          })
     });
 }

正常解析出图片地址后,只要转为 Markdown 格式,就可以插入到消息框中。但是,这里有一个坑,比如你在 QQ 里既包含文本,也包含图片的消息中,拉动选中图片后复制,再粘贴到摸鱼派的编辑框中,就会出现这种样子: ![](file:///C:\Users\XXXXXXX\Tencent Files\XXXXX\Image\Group2\XR\{2\XR{2({Q@PASEGXI06VVTJSV.png)

这是因为 QQ 消息使用 HTML 富文本渲染,而图片是引用的本地图片,因此造成解析出来的地址是本地图片的磁盘路径。对于网页,这个问题自然是无法解决。但是 Electron 可以访问文件系统,所以可以再进一步处理。这里使用了一个 get-file-object-from-local-path 的 node 库[2],可以将本地文件转为 File 对象:

files = files.map(f => 
        f.startsWith('file:') ? 
           constructFileFromLocalFileData(
               new LocalFileData(f.replace(/file:\/\/\//g, ''))
           ) : f
)
file = file.concat(files);

这里检查了路径的协议,如果是 file:// 协议,则将 file:// 移除,剩下的就是文件的磁盘路径了,然后使用库的 LocalFileData 生生成一个 FileData 再使用 constructFileFromLocalFileData 方法转为 Web API 的 File 对象即可。这样,如果是本地的图片,就会被上传再取得网页地址插入编辑框中。

复制图片

如果是网页,据我目前了解,无法将图片复制到剪贴板中,只有浏览器原生功能可以做到。而 Electron,则提供了一套 Clipboard API,可以精细操作剪贴板。[3]

要做到复制图片,首先要拿到图片的地址或对象,因此,我将功能加入到图片的右键菜单中。后面有机会再单独讲一下 Electron 右键菜单的实现,这里暂且略过。总之,我们通过右键菜单的操作拿到了要复制的图片的 Image 标签对象。接着就可以来实现复制图片的功能。

async copyImg(img) {
    // 获取图片的 buffer
    const imgBuffer = await urlToBuffer(img.src);
    // 获取图片的原始宽高
    const { naturalWidth: width, naturalHeight: height } = img;
    // 生成一串 img 标签 HTML 代码
    const htmlContent = `<img src="${img.src}" width="${width}" height="${height}" />`;
    // 写入剪切板
    clipboard.write({
        // 写入富文本
        html: htmlContent,
        // 写入图片文件
        image: nativeImage.createFromBuffer(imgBuffer, {
            width,
            height,
        }),
    });
}

这里分为三个步骤:

  1. 获取图片 buffer;
  2. 组建 img 标签 HTML
  3. 写入剪贴板;

写入剪贴板包含了 image 和 html 两种格式,为什么写入图片要同时写入两种格式呢?这是经过实验得出的做法:如果使用 html 格式写入,则在画图,Photoshop 之类的图片处理工具是无法粘贴的,而如果只是使用 image 的格式,则会出现 Gif 图片无法写入粘贴,我们在网页上使用复制 GIF 图片其实也可以发现,粘贴后图片是不会动的。因此,为了解决这个问题,就分为了 html 和 image 两种格式同时写入。这样无论是静态图还是动态图,都可以正常复制,并且可以粘贴到其他软件上使用。

不过对于动图的复制,还是有些悬而未解的问题,复制的 GIF 只能用于粘贴在一些富文本的编辑软件,QQ 微信之类仍然无法粘贴。还在持续研究,待解决后再更新了~

[1] Web API 接口参考 Element: paste 事件
[2] Node 库 get-file-object-from-local-path
[3] Electron Clipboard API

本文首发于:https://my.hancel.org/2023/05/22/electron-copy-and-paste.html