前端通过 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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容