8

又get到了,JS复制图片到剪切板

 6 months ago
source link: https://www.zhangxinxu.com/wordpress/2023/09/js-copy-image-clipboard/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=11015 鑫空间-鑫生活
本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可以联系授权。

封面图 鳐鱼

一、总要有原因的

为什么会突然想到了解如何使用JS复制图片到剪切板呢?

因为上周弄了个PNG/JP在线压缩的工具,一开始的时候,图片获取的途径只能是下载到本地。

但很多时候,我是希望这种压缩好的图片可以直接上传,不需要再从本地转一圈。

此时,如果有一个复制功能就好了。

直接点击一个按钮,压缩好的图片复制到剪切板,然后再去上传页面进行上传,多方便,多轻松。

所以,就去了解了下,还是学到了不少东西的。

这里和大家分享下。

首先,先从简单的开始。

二、原生的图片复制API

浏览器提供了一个名为 ClipboardItem 的实例对象,可以构造构造剪切板复制对象。

例如一段文本的复制(代码取自MDN文档):

function setClipboard(text) {
  const type = "text/plain";
  const blob = new Blob([text], { type });
  const data = [new ClipboardItem({ [type]: blob })];

  navigator.clipboard.write(data).then(() => {
    /* success */
  }, () => {
    /* failure */
  });
}

图片也不在话下。

我专门写了个复制图片的方法,参数就是页面中的IMG元素对象。

const doCopyImg2Clipboard = async (image, success = () => {}, failure = () => {}) => {
    if (!image || !image.src) {
        return;    
    }
    
    // 需要复制的图片的地址
    const src = image.src;
    // 请求该地址,返回图片的blob数据
    const response = await fetch(src);
    
    // 需要是blob数据格式
    const blob = response.blob();
    
    // 使用剪切板API进行复制
    const data = [new ClipboardItem({
        [blob.type || 'image/' + src.split('.').slice(-1)[0].replace('jpg', 'jpeg')]: blob    
    })];
    
    navigator.clipboard.write(data).then(success, failure);
}

如果你已经有了图片的数据(例如本地上传、拖拽或粘贴的图片),则fetch步骤可以略过。

所以,当页面中有如下所示的HTML代码(一张图,一个按钮):

<button id="button" type="primary">复制图片</button>
<img id="image" src="./mybook.jpg">

就可以使用一个简单的点击事件,触发图片的复制了。

// 点击按钮进行复制
button.addEventListener('click', () => {
    doCopyImg2Clipboard(image, function () {
        new LightTip('复制成功', 'success');
    }, function (err) {
        new LightTip('复制失败:' + err, 'error');
    });
});

然而,上面的代码得到的结果却是失败!

复制失败

从上面的错误提示可以看出,浏览器是不支持 jpg 格式的图片信息写入剪切板的。

试试PNG呢?

如果我们把图片的JPG格式,转换为PNG格式,再试一试,比方说:

<button id="button" type="primary">复制图片</button>
<img id="image" src="./mybook.png">

则就可以成功复制!

眼见为实,您可以狠狠地点击这里:复制JPG和PNG图片到剪切板是否成功demo

比方说,我们点击demo右侧的那个按钮,此时,提示的就会是复制成功,如下截图示意。

复制成功提示

此时,PNG图片信息就已经在剪切板中了。

我们可以找个富文本输入框,或者在线文档编辑器等进行测试。

例如demo页面提供的输入框,我们执行下Ctrl+V,则可以看到图片出现了,如下图所示。

复制粘贴到输入框

OK,好,下面问题来了,页面中有些图片他就是JPG格式的,我非要复制,有没有什么办法呢?

三、JPG/WebP图片的解决之道

要想让JPG或者webp格式的图片也支持剪切板复制,很简单,转为PNG无损图就好了呀。

例如,绘制在canvas上,再使用canvas的toBlob()方法转一下就噢啦。

所以,上面提供的复制代码,我们还可以进一步增强下。

完整JS代码如下所示。

const doCopyImg2Clipboard = async (image, success = () => {}, failure = () => {}) => {
    if (!image || !image.src) {
        return;    
    }
    
    // 原图尺寸
    const { naturalWidth, naturalHeight } = image;
    
    if (!naturalWidth) {
        failure('图片尚未成功加载');
        
        return;
    }
    
    // 绘制图片到canvas上
    const canvas = document.createElement('canvas');
    canvas.width = naturalWidth;
    canvas.height = naturalHeight;
    // canvas绘制上下文
    const context = canvas.getContext('2d');
    // 图片绘制
    context.drawImage(image, 0, 0, naturalWidth, naturalHeight);
    // 转为Blob数据
    canvas.toBlob(blob => {
        // 使用剪切板API进行复制
        const data = [new ClipboardItem({
            ['image/png']: blob    
        })];
        
        navigator.clipboard.write(data).then(success, failure);
    });
}

此时,无论是JPG图片还是WebP图片都可以复制到剪切板了。

doCopyImg2Clipboard(image, function () {
    console.log('复制成功');
});

有演示页面可以体验,您可以狠狠地点击这里:JS复制JPG图片到剪切板demo

复制JPG图片

然而,虽然图片复制了,但是变成PNG之后,原本尺寸适合的JPG图片尺寸就会变得很大。

那图片等于白压缩了。

所以,如果希望复制JPG图片的时候,保留该JPG原始的尺寸,该怎么办呢?

四、若想要保留原图尺寸?

解决方法是不是复制blob二进制数据,而是复制表示图片数据的base64字符串。

然后在上传或者粘贴的地方再二次处理。

图片转base64并复制到剪切板

不哔哔,直接看代码,使用FileReader对象将blob数据转为base64。

const doCopyImgBase64Clipboard = (image, success = () => {}, failure = () => {}) => {
    if (!image || !image.src) {
        return;    
    }
    
    // 需要复制的图片的地址
    const src = image.src;
    // 请求该地址,返回图片的blob数据
    fetch(src).then(response => response.blob()).then(blob => {
        // blob数据转base64
        const reader = new FileReader();
        reader.onload = function() {
            navigator.clipboard.writeText(this.result).then(success, failure);
        };
        reader.readAsDataURL(blob) ;
    });
}

image 表示页面中的IMG元素对象。 success 表示成功的回调 failure 表示失败的回调

此时,就可以点击按钮,一键复制。

base64图片的处理

在这场场景下,复制并不是最难的,因为文本复制有着非常成熟的解决方案,比方说我之前弄了个“极简文字内容剪切板复制”的gitee项目,优点是自带复制成功的交互提示。

也可以使用业界知名的clipboard.js:https://github.com/zenorocha/clipboard.js

难点在于粘贴时候对base64文本的处理,因为处理粘贴文本并不是一个经常会遇到的场景,大多数的前端都缺乏相应的处理经验。

这里,通过两个例子,演示预览和上传的处理。

1. 预览

base64本身就可以作为图片的URL地址,因此,预览相对简单,只要创建个图片DOM元素,然后插入到页面中就好了。

那如何获得剪切板中粘贴的内容呢?有对应的API的,代码示意:

// 在输入框中粘贴
input.addEventListener('paste', event => {
    var paste = event.clipboardData?.getData('text') || '';
});
// 在页面中粘贴
document.addEventListener('paste', event => {
    var paste = event.clipboardData?.getData('text') || '';
});

此时,只要判断 paste 内容匹配 base64 图片格式即可。

以在输入框中粘贴base64图片为例,完整交互JavaScript代码参见:

// 粘贴对base64内容的监控处理
input.addEventListener('paste', event => {
    var clipboardData = event.clipboardData;

    var paste = clipboardData?.getData('text') || '';

    if (!/^data:image\/[a-z]+;base64,/.test(paste)) {
        return;
    }
    
    event.preventDefault();
    
    // 我是范围
    const selection = document.getSelection();
    // 我是选区
    const range = selection.getRangeAt(0);
    // 删除选区内容,
    range.deleteContents();
    // 并替换成图片
    const imgNode = document.createElement('img');
    imgNode.src = paste;
    range.insertNode(imgNode);
    // 光标定位在图片后面
    range.setStartAfter(imgNode);
});

眼见为实,您可以狠狠地点击这里:JS复制图片base64信息到剪切板demo

点击按钮,会复制图片的base64信息,此时,再粘贴到页面中的测试输入框,会发现,显示的不是字符串内容,而是图片,就是因为有上面这段 JS 解析代码。

操作示意

2. 上传

base64图片地址上传有两种解决方法,一是后端判断处理,即,如果post的是二进制数据流,如何处理,如果post的是字符串数据流,又该如何处理。

还有一种方法是在前端处理,将剪切板中的base64字符串转为blob文件数据再上传,这样,后端那边轻松点。

以下代码示意了下如何处理:

// base64 转 blob
const b64toBlob = (b64Data, contentType='', sliceSize=512) => {
  const byteCharacters = atob(b64Data);
  const byteArrays = [];

  for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
    const slice = byteCharacters.slice(offset, offset + sliceSize);

    const byteNumbers = new Array(slice.length);
    for (let i = 0; i < slice.length; i++) {
      byteNumbers[i] = slice.charCodeAt(i);
    }

    const byteArray = new Uint8Array(byteNumbers);
    byteArrays.push(byteArray);
  }

  const blob = new Blob(byteArrays, {type: contentType});
  return blob;
}

// 粘贴base64上传
document.addEventListener('paste', function (event) {
    var clipboardData = event.clipboardData;

    var paste = clipboardData?.getData('text') || '';

    if (!paste.startsWith('data:image')) {
        return;
    }

    // 核心数据
    var base64 = paste.split('base64,')[1];
    // 类型
    var type = paste.split(';base64')[0].replace('data:', '');
    // 获取 blob 数据
    var blob = b64toBlob(base64, type);
    // 设置个文件名,不然是undefined
    blob.name = new Date().toLocaleString().replaceAll('/', '-').replace(/\s/g, '_').replaceAll(':', '') + '.' + (type.split('/')[1] || 'png');

    // 此时的 blob 就是需要上传的文件数据了...
});

于是,整个一套流程就可以走通了。

前端压缩图片,JS复制压缩后的base64字符信息,然后JS解析base64并上传。

时不时给自己找个需求,而不仅仅是业务开发,可能会遇到平常遇不到的场景,积累不一样的经验。

要是下次业务开发遇到类似需求,那就是分分钟上手,开发效率领先一步。

还能撸一篇文章,完成自己给自己制定的平均每周需要一个技术文章输出的KPI。

一劳多得,岂不美哉。

噢啦,以上就是本文的全部内容啦。

希望里面的内容可以对大家的工作学习有所帮助。

如果您觉得不错,欢迎,点赞。

😋 😛 😝 😜 🤪

(本篇完)1f44d.svg 是不是学到了很多?可以分享到微信
1f44a.svg 有话要说?点击这里


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK