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

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

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

前言✨

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

前置知识✨

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

1.FileReader对象   MDN-FileReader

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

  • FileReader.readAsDataURL 方法会读取指定的 BlobFile 对象
  • 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监视,导致浏览器错以为代码发生改变而自动刷新了

f40e9eaa2c884d00b9737fc573f92a19_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.gif

展示效果:

40adc95cd94a42bda8eba3a2cd5c2888_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.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.2 encoder0.3 encoder0.4 encoder0.5 默认值0.92
jng 158kb 51kb 66kb 75kb 87kb 190kb
gif 490kb 55kb 72kb 85kb 99kb 410kb
png 118kb 141kb 141kb 141kb 141kb 141kb
other 76kb 38kb 47kb 50kb 60kb 100kb

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

  • encoder系数在0.2-0.5之间可以有效的压缩图片体积
  • encoder默认值体积反而会变大
  • png格式文件,和encdoer系数无关,甚至体积不减反增这里就展示了0.2~0.5的数据 低于0.2会出现图片压缩过度,质量下降。

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

image.pngimage.png

5.发现问题

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

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

image.png

我们的结论一定正确吗?

再来看看这张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

总结✨


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


相关文章
|
7月前
|
JavaScript 前端开发
VUE3(三十六)压缩base64格式图片
VUE3(三十六)压缩base64格式图片
592 0
|
2月前
|
存储 XML Java
如何在 Java 中将常见文档转换为 PNG 图像数组
如何在 Java 中将常见文档转换为 PNG 图像数组
20 1
|
5月前
鸿蒙base64编码字符集转化成图片文件
鸿蒙base64编码字符集转化成图片文件
148 0
|
开发者 Kotlin
将JPG图像根据色彩范围转换为PNG透明图像(kotlin)
这实际上是一个十分普遍的需求,在kotlin中如何完成这一任务呢?其实这样简单的操作不需要任何三方库,只需要BufferedImage的原生功能就能做到,约60行代码
204 0
|
JavaScript 前端开发
用Vue框架将base64格式的图片画到canvas上
用Vue框架将base64格式的图片画到canvas上
262 0
|
7月前
|
前端开发 JavaScript 小程序
前端图片转base64 方法
前端图片转base64 方法
217 0
|
JavaScript
js中将byte数组转换成base64图片显示
js中将byte数组转换成base64图片显示
|
存储 缓存 算法
图片转为base64格式的优缺点分析
图片转为base64格式的优缺点分析
570 0
|
Web App开发 前端开发
|
Web App开发 JavaScript 前端开发
如何用Javascript将图片的绝对路径转换为base64编码
我们可以使用canvas.toDataURL的方法将图片的绝对路径转换为base64编码;
271 0
如何用Javascript将图片的绝对路径转换为base64编码