canvas还能这么用?🤨 图片压缩70% | base64转换原理

·  阅读 3686
canvas还能这么用?🤨 图片压缩70% | base64转换原理

我正在参与掘金创作者训练营第4期,点击了解活动详情,一起学习吧!

图片上传功能在日常的开发中并不少见,但是图片的体积过大会增大服务器压力,用户体验感也不好,本文将基于canvas实现图片的压缩。全文无尿点,可以放心观看。

前言✨

之前和室友做了一个Vue3的蛋糕售卖和后台管理的系统,后台服务涉及到了管理员图片的上传,最开始采用base64直接上传,服务器响应时间太长(之前没经验),后来可以选择直接传输file类型的文件,但是管理员上传的文件大小不受控制,文件较大的时候,服务器响应时间也比较久。摸爬滚打的参考了很多的文章之后,也踩了不少的坑,于此记录下来,分享技术的同时。也算是自己的一个小总结

前置知识✨

主要简单介绍一下后续所使用到的对象/方法,尽量用最简单最浅显的语言来讲清楚这部分知识,可放心食用。

1.FileReader对象 MDN-FileReader

通过FileReader对象可以读取文件/缓冲区内容

  • FileReader.readAsDataURL 方法会读取指定的 Blob 或 File 对象

  • FileReader.onload: 读取完毕之后的回调函数,返回的数据类型长这样(先接着往下看)

image.png

2.canvas MDN-canvas

可以理解为一个标签,可以通过这个标签上的属性来绘制图片并且可以实现压缩效果。

  • canvas.getContext('2d'): 获得渲染上下文和它的2d绘画功能,返回ctx对象
  • ctx.drawImage(img,x,y,width,height): 绘制图像 | x,y对应的是坐标轴,width、height绘制的图片大小
  • canvas.toDataUrl(type, encoderOptions): 返回一个包含图片展示的 data URI
var canvas = document.getElementById("canvas");
var dataURL = canvas.toDataURL();
console.log(dataURL); // 注意返回结果中的逗号(伏笔)
// "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNby
// blAAAADElEQVQImWNgoBMAAABpAAFEI8ARAAAAAElFTkSuQmCC"

type: 指定图片的类型 | encoderOptions(可选) 指定图片体积的压缩比(0~1)默认为0.92

本文的重点就是基于encoderOptions进行压缩图片

3.atob MDN-atob

对经过 base-64 编码的字符串进行解码

let encodedData = window.btoa("Hello, world"); // 编码  SGVsbG8sIHdvcmxk
let decodedData = window.atob(encodedData);    // 解码  Hello, world

4.ArrayBuffer MDN-ArrayBuffer

ArrayBuffer对象代表储存二进制数据的一段内存

const buf = new ArrayBuffer(8);

上面代码生成了一段 8 字节的内存区域,每个字节的值默认都是 0。(看不太懂,不着急👇接着往下看)

5.File 构造函数 MDN-File

var myFile = new File(bits, name[, options]);
  • bits: 一个包含ArrayBufferArrayBufferViewBlob,或者 DOMString 对象的 Array — 或者任何这些对象的组合。这是 UTF-8 编码的文件内容。

  • name: 表示文件名称,或者文件路径。

  • options: 选项对象,包含文件的可选属性。

正文✨

1. 搭建一个基于koa的后台服务|接受图片并且返回 图片的url地址

如果不了解koa的童鞋,可以讲这段代码拷贝,安装一下对应的依赖启动服务即可。

  • koa-router 后端路由服务
  • koa-static 开启静态服务
  • koa-body 读取file文件
const path = require('path')
const Koa = require('koa')
const KoaStatic = require('koa-static')
const Router = require('koa-router')
const koaBody = require('koa-body')

const router = new Router()
const app = new Koa()

app.use(koaBody({
  multipart: true,// 支持请求中body携带文件类型的数据
  formidable: {
    uploadDir: path.join(__dirname, '/public/uploads'),//设置图片保存地址
    keepExtensions: true
  }
}))
// 接受图片保存在public文件夹下,开启静态web服务
app.use(KoaStatic(path.join(__dirname, '/public'))) 

// 上传图片中间件
function uploadImg (ctx) {
  const file = ctx.request.files.file
  const basename = path.basename(file.path)
  // 根据图片的绝对地址获取图片名称: 例如 /d/aa/c.js  -> c.js
  ctx.body = { url: `${ctx.origin}/uploads/${basename}` }
}
// cors 跨域解决方案
app.use(async (ctx, next) => {
  ctx.set('Access-Control-Allow-Origin', '*');
  if (ctx.method == 'OPTIONS') {
    ctx.body = 200;
  } else {
    await next();
  }
})
router.post('/uploads', uploadImg)

app.use(router.routes())
app.listen(9001, () => console.log('服务启动成功'))
// http://localhost:9001/uploads

目录结构:

image.png

2.上传图片,返回图片的url地址之后渲染到页面中(模拟一次正常的网络交互)

<input type="file" accept="image/*" name="file">
<button onclick="submit">提交表单</button>
<img id="img" src="" alt="">

First make it work,then make it fast 我们先让流程跑通

 const input = document.querySelector('input')
    const img = document.getElementById('img')
    const btn = document.querySelector('button')
    const formData = new FormData()

    function reqUploadImg() {
      fetch('http://localhost:9001/uploads', {
        method: 'post',
        body: formData
      }).then(res => res.json())
        .then(res => {
          console.log(res)
          img.src = res.url
        })
    }
    input.onchange = function (e) {
      const file = e.target.files[0]
      formData.append('file', file)
    }

上面的代码需要注意几个内容(正常交互不必在意)

  • 开启liveServer
  • 提交表单文件FormData 添加表单内容为 append 方法(我找了好久。。。用set方法一直为空)
  • 不能讲Service文件夹和前台代码放在一个根目录下,否则每次提交会刷新,原因是liveServer的监视,如下

可以看到,每次提交之后因为koa后台服务保存了图片之后,进而被liveServer监视,导致浏览器错以为代码发生改变而自动刷新了

12ea546d4c3331273658a13d07d1ee19 (1).gif

展示效果:

QQ录屏20220226221408.gif

3.优化代码

写代码之前先捋清楚我们的思路

  • 1.首先我们需要读取图片
  • 2.读取完文件之后通过canvas压缩生成一个新的图片
  • 3.再通过buffer重新转化为图片

先看看效果,打印两次文件的size大小,从158kb到52kb,体积变为原来的1/3不到

image.png

修改后的代码(后面会讲base64转化原理)

input.onchange = function (e) {
      const file = e.target.files[0]
      // console.log(file)
      compressPic(file).then(resultFile => {
        formData.append('file', resultFile)
      })
    }
    function compressPic(file, encoderOtp = 0.2) {
      return new Promise(resolve => {
        // 1. 通过FileReader读取文件
        const reader = new FileReader()
        let res = reader.readAsDataURL(file)
        reader.onload = (event) => {
          // 2. 读取完毕之后获取图片的base64(上文伏笔),并创建新图片
          const { result: src } = event.target
          const image = new Image()
          image.src = src
          image.onload = () => {
            // 3.图片加载完之后通过canvas压缩图片
            const canvas = document.createElement('canvas')
            canvas.width = image.width
            canvas.height = image.height
            // 3.1 绘制canvas
            const ctx = canvas.getContext('2d')
            ctx.drawImage(image, 0, 0, image.width, image.height)
            // 3.2 返回图片url地址,并且进行压缩
            const canvasURL = canvas.toDataURL(file.type, encoderOtp)
            const buffer = atob(canvasURL.split(',')[1])
            // 3.3 bufferArray 无符号位字节数组 相当于在内存中开辟length长度的字节空间
            let length = buffer.length
            const bufferArray = new Uint8Array(length)
            // 3.4 给新开辟的bufferArray赋值
            while (length--) {
              bufferArray[length] = buffer.charCodeAt(length)
            }
            // 3.5将压缩后的文件通过resolve返回出去
            const resultFile = new File([bufferArray], file.name, { type: file.type })
            console.log(resultFile)
            resolve(resultFile)
          }
        }
      })
    }

4.测试结果

我们再测试其他的几张图片试一试

文件格式原体积encoder0.2encoder0.3encoder0.4encoder0.5默认值0.92
jng158kb51kb66kb75kb87kb190kb
gif490kb55kb72kb85kb99kb410kb
png118kb141kb141kb141kb141kb141kb
other76kb38kb47kb50kb60kb100kb

结论 -----这部分结论可以参考 :# 前端图片最优化压缩方案

  • encoder系数在0.2-0.5之间可以有效的压缩图片体积
  • encoder默认值体积反而会变大
  • png格式文件,和encdoer系数无关,甚至体积不减反增

这里就展示了0.2~0.5的数据 低于0.2会出现图片压缩过度,质量下降。

左图为encoder0.1 右图为0.2,当encoder为0.1的时候,仔细看看还是可以看出丢失的精度。

5.发现问题

注意看第三行测试用例,为什么无论encoder是什么值,压缩后的体积都是一样呢? 🤔🤔

看来这其中一定哪个环节出了问题,继续测试发现只要是png格式的文件就会出现这样的情况,其他文件即便是gif也会进行压缩

我们的结论一定正确吗?

再来看看这张png格式文件

image.png

再试试体积很小的svg文件

image.png

经过多组测试发现,使用canvas压缩体积的时候,文件体积过小反而会出现越压缩体积越大的情况

6.解决小体积图片压缩问题

那这样就很好解决了,设置一个体积阀门,小于这个体积的图片我们不做限制。毕竟我们的初心就是为了压缩大体积文件

在上述代码的基础上添加 minSize,当小于300kb的时候不进行canvas压缩

image.png 这里其实也可以通过Promise.all实现多文件的压缩。这里就不演示了

base64转换原理

说出来也挺巧的,在写这篇文章的时候恰好看到大佬山月在朋友圈发的base64转换工具,感觉很棒!这里我们就借助这个工具来和大家讲讲base64的转换原理。地址:base64转换工具

image.png

转换流程:

  • 将数字/字母转化为 Asc编码、汉字对应16位两字节的方式
  • 将AsCii编码转化为base64编码。编码的方式为:
    • 每 3*8bit 的字节转换为 4*6bit 的字节,剩下的两位用 00 补齐,所以Base64 编码后的数据比编码之前大 1/3
    • 每不足三个字节,自动补全,结果采用=占位 如👇输入字母a 转化为 YQ==

image.png

总结✨

image.png

  • 基于canvas可以实现对大图片的体积压缩 | 甚至可以实现指定drawImage图片宽高,进一步缩小图片体积
  • 图片体积过小大概小于 150kb以下,会出现越压缩体积越小的情况 (这部分没有太多数据支撑,可能会存在一定误差)
  • 基于binary-to-text算法,可以将二进制数据转化为64格式,但是体积也会增大1/3

感谢看完,如果对你有帮助的话,点个👍再走呀~ 倔友😁

分类:
前端
分类:
前端
收藏成功!
已添加到「」, 点击更改