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


相关文章
|
编解码 JavaScript 前端开发
jsQR 一个完全独立的javascript 二维码识别库
jsQR 是一款纯粹的由javascript实现的二维码识别库,可以在浏览器端使用,也可以在后端node.js环境使用。我之前使用过其他的识别库,例如:qrcode-reader 或其他,在使用上都比较麻烦,而且识别率并不高。jsQR是后来发现的,感觉(没有实际对比验证)jsQR识别率要更高些,使用起来也更简单,不需要安装其他依赖软件。
jsQR 一个完全独立的javascript 二维码识别库
|
API 开发者
工作日和节假日api
节假日api核心服务托管在阿里云之上,API天然分布式、高可用。
|
JSON atlas 图形学
unity之spine骨骼动画使用
unity实现spine骨骼动画使用
unity之spine骨骼动画使用
|
缓存 Linux 开发工具
CentOS 7- 配置阿里镜像源
阿里镜像官方地址http://mirrors.aliyun.com/ 1、点击官方提供的相应系统的帮助 :2、查看不同版本的系统操作: 下载源1、安装wget yum install -y wget2、下载CentOS 7的repo文件wget -O /etc/yum.
261425 0
|
人工智能 C++ iOS开发
ollama + qwen2.5-coder + VS Code + Continue 实现本地AI 辅助写代码
本文介绍在Apple M4 MacOS环境下搭建Ollama和qwen2.5-coder模型的过程。首先通过官网或Brew安装Ollama,然后下载qwen2.5-coder模型,可通过终端命令`ollama run qwen2.5-coder`启动模型进行测试。最后,在VS Code中安装Continue插件,并配置qwen2.5-coder模型用于代码开发辅助。
20095 71
|
8月前
|
前端开发 JavaScript 应用服务中间件
前端跨域问题解决Access to XMLHttpRequest at xxx from has been blocked by CORS policy
跨域问题是前端开发中常见且棘手的问题,但通过理解CORS的工作原理并应用合适的解决方案,如服务器设置CORS头、使用JSONP、代理服务器、Nginx配置和浏览器插件,可以有效地解决这些问题。选择合适的方法可以确保应用的安全性和稳定性,并提升用户体验。
5710 90
|
JavaScript 架构师 前端开发
为什么“低代码”是未来趋势?
【10月更文挑战第17天】
289 0
为什么“低代码”是未来趋势?
|
小程序 前端开发
【非常全】微信小程序下载图片到相册,微信小程序自动获取分享图片到相册
【非常全】微信小程序下载图片到相册,微信小程序自动获取分享图片到相册
1204 3
【Bug】ERROR ResizeObserver loop completed with undelivered notifications.
【Bug】ERROR ResizeObserver loop completed with undelivered notifications.