背景
这个版本接了个需求,针对制图工具页面在图片上传时做图片格式转换和压缩优化。制图工具支持从本地上传图片,也支持复制网络图片粘贴上传,目前的现状是只能上传 5M 以内的图片,实际的运营场景使用的图片都是大于 5M 的,而且通过网络复制的图片通常会比从网络上下载到本地之后的图片体积要大很多,导致运营不能通过复制上传成功,需要先下载,这就造成运营工作效率比较低,才有这次的需求出现。
转化png图片为jpg
获取剪贴版的数据代码 e.clipboardData || window.clipboardData
,看下从剪贴板上获取的文件信息如下:
// 粘贴版上拿到的图片数据
// lastModified: 1704701662273
// lastModifiedDate: Mon Jan 08 2024 16:14:22 GMT+0800 (中国标准时间) {}
// name: "image.png"
// size: 5520974
// type: "image/png"
// webkitRelativePath: ""
const items = (e.clipboardData || window.clipboardData).items
const fileList = []
if (!items || items.length === 0) {
MedusaMessage.error('当前浏览器不支持')
return
}
// 搜索剪切板items
for (let i = 0; i < items.length; i += 1) {
if (items[i].type.indexOf('image') !== -1) {
fileList.push(items[i].getAsFile())
}
}
if (fileList.length <= 0) {
MedusaMessage.error('粘贴内容非图片')
return
}
return fileList
可以看到从剪贴板上获取一个png图片的数据是 5M 多,但实际下载下来的图片只有2M多
这样从本地其实可以上传上去的,但既然后台提供了复制粘贴图片的功能,就不能这么操作,所以我们的方案是把图片转换成 jpg 的格式,jpg 格式图片相对于 png 在体积上会小很多,而且制图工具其实不需要 png 的格式,本身制图工具就是做裁剪和白底图制作的。基于这样一个背景,转换图片格式是可行的。
这里通过 Canvas 的 API 来操作,文件是数组格式的,所以在处理图片的时候需要批量操作,使用 FileReader 读取文件,因为读取文件是异步的,所以需要返回一个 Promise,通过记录 count 的方式来判断数组是不是读取完毕,然后将转化之后的数据返回。
在开始做的时候,我是通过 canvas.toDataURL
将图片读出 base 64格式的图片,然后再通过 new Blob 的方式将数据一下传给 new File 变成 File 的格式,但是在上传的时候发现,通过 new Image()
打开时,图片是不能加载的,查了文档之后发现可以直接通过 canvas.toBlob
的方式将数据包成 blob 格式,不用通过构造函数的方式。经常不操作 Canva ,对其常用 API 也陌生了。
将剪切板的图片格式转为 jpg 格式的完整的代码如下:
// 转换图片为 jpeg 格式
const handleFormatFile = (files) => {
return new Promise((resolve, reject) => {
// 创建img元素
const img: any = new Image()
// 获取图片数据
const reader = new FileReader()
let count = 0
const results: any = []
reader.onload = (e) => {
img.crossOrigin = 'anonymous' // 设置跨域的图片可以读出
img.src = e.target?.result
const file = files[count]
img.onload = () => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = img.width
canvas.height = img.height
ctx?.drawImage(img, 0, 0)
canvas.toBlob(
(jpegData: any) => {
const formatFile = new File([jpegData], `${file.name.replace(/\.[^/.]+$/, '')}.jpg`, {
type: 'image/jpeg',
})
count++
results.push(formatFile)
if (count === files.length) {
resolve(results)
}
},
'image/jpeg',
1,
)
}
}
reader.onerror = () => {
reject(new Error('转化图片格式出错'))
}
Array.from(files).forEach((file: any) => {
reader.readAsDataURL(file)
})
})
}
压缩图片
上传到素材库的图片大小限制是 10M,但是同步到 spu 的图片限制是 5M,所以接下来在上传的时候需要对图片进行压缩,降低图片的质量,同样 canvas.toBlob 可以设置压缩的质量,针对高度超过 16000的图片进行等比例的缩放,
使用图片的时候 img.crossOrigin = 'anonymous'
一定要设置为跨域,因为我们很多图片都是来自 cdn 的:
// 这里压缩图片超过10M的图片质量
const compressImgQuality = async (ctx: any) => {
return new Promise((resolve) => {
const { currentImg } = ctx
fetch(ctx.currentImg.url)
.then((r) => r.blob())
.then((tempBlob) => {
if (Math.ceil(tempBlob.size / (1024 * 1024)) > 5) {
const img = new Image()
img.crossOrigin = 'anonymous' // 设置跨域的图片可以读出
img.src = currentImg.url
img.onload = () => {
const canvas = document.createElement('canvas')
const canvasCtx = canvas.getContext('2d')
// 校验高度
const maxHeight = 16000
let newWidth = currentImg.width
let newHeight = currentImg.height
if (currentImg.height > maxHeight) {
// 将图片等比例缩放
const scaleFactor = Math.min(1, maxHeight / img.height)
newWidth = Math.floor(img.width * scaleFactor)
newHeight = Math.floor(scaleFactor * img.height)
}
canvas.width = newWidth
canvas.height = newHeight
canvasCtx?.drawImage(img, 0, 0)
canvas.toBlob(
async (blob: any) => {
const tempFile = new File([blob], '素材图片.jpeg')
const res = await ctx.uploader.upload({ file: tempFile }, 'img')
const resizeObj = {
url: res.url,
width: newWidth,
height: newHeight,
}
resolve(resizeObj)
},
'image/jpeg',
0.8,
)
}
img.onerror = () => {
resolve({ url: '' })
}
} else {
resolve(currentImg)
}
})
})
}
脚本拼接测试图片
代码写完了,但是怎么测试呢,这次需求是要把大小 5M 的限制放开到 10M ,上哪去找那么大的图片呢。找了半天也没找到,所以就有个想法,找不到这么大的图片就自己把两张图片拼接一下吧,使用 Node 的 jimp 库,这个 Node的图像处理库完全用 JavaScript 编写,没有本地依赖项。 找个以前的项目配置下 index.js ,然后在 package.json 里面配置命令:splice-img: 'node index.js'
const Jimp = require('jimp');
async function mergeImages() {
try {
const image1 = await Jimp.read('1.jpg');
const image2 = await Jimp.read('2.jpg');
const canvasWidth = Math.max(image1.getWidth(), image2.getWidth());
const canvasHeight = image1.getHeight() + image2.getHeight();
const canvas = new Jimp(canvasWidth, canvasHeight);
canvas.blit(image1, 0, 0);
canvas.blit(image2, 0, image1.getHeight());
await new Promise((resolve, reject) => {
canvas.write('new.jpg', (err) => {
if (err) reject(err);
resolve();
});
});
console.log('图片拼接完成!');
} catch (err) {
console.error(err);
}
}
mergeImages();
上面这个脚本就是将2张图片上下拼接在一起,这样拼接高度和大小就都符合要求了,开始测试吧。这个脚本也可以给测试同学用,毕竟他们找这种图片也是很费劲的。
温故知新
下面列举了在操作文件和图片会经常用到的 API,罗列的目录,根据目录去刷下这些 API 的应用场景和使用方法,多操作,多练习,这样映像就深了。
canvas 的常用 API
canvas 元素的基本用法
绘制图形和文本
图形变换和合成操作
渐变和阴影效果
像素操作和图像处理
Blob 类型的用法
Blob 对象的概述
创建和操作 Blob 对象
Blob 对象的常见应用场景
FileReader 的用法
FileReader 对象的基本原理
读取文本和二进制数据
处理读取过程中的事件
FileReader 的兼容性问题和解决方案
new File 的用法
File 对象的创建和属性
使用 File 对象进行文件操作
File 对象与其他相关 API 的配合使用
new Image 的用法
Image 对象的创建和基本属性
加载图像和处理加载过程中的事件
图像的显示和操作
Image 对象的应用场景和注意事项
以上的用法可以直接看 MDN 上的文档,这里就不做搬运工了,有空还是需要经常刷一下 MDN 的,熟能生巧,长时间不用,确实会忘记。
参考:
https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API
https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader
https://developer.mozilla.org/zh-CN/docs/Web/API/Blob
https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLImageElement/Image