前端通过 Canvas 转换图片格式

背景

这个版本接了个需求,针对制图工具页面在图片上传时做图片格式转换和压缩优化。制图工具支持从本地上传图片,也支持复制网络图片粘贴上传,目前的现状是只能上传 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多


image.png

这样从本地其实可以上传上去的,但既然后台提供了复制粘贴图片的功能,就不能这么操作,所以我们的方案是把图片转换成 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

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容