canvas节点无法导出图片_前端实现图片压缩及遇到的问题

2023-05-16

图片上传是前端中常见的的业务场景。无论是前台还是后台,适当的对图片进行压缩处理,可以显著的提升用户体验。而在后台管理系统中,图片压缩不仅仅能够提升后台管理员操作体验,更是可以防止后台设置过大的图片导致前台图片加载过久,从而影响用户体验。

之前我在网上冲浪的时候,经常会看到一些关于如何实现前端图片压缩的文章,基本大概的流程都是:

  1. input 读取到 image/file ,使用 FileReader 将其转换为 base64 编码
  2. 新建 img ,使其 src 指向刚刚的 base64
  3. 新建 canvas ,将 img 画到 canvas 上
  4. 利用 canvas.toDataURL/toBlob 将 canvas 导出为 base64 或 Blob
  5. 将 base64 或 Blob 转化为 File


基本的流程就是这样,也只能是这样了。实现图片压缩的关键是在 canvas.toDataURL/toBlob 这两个 API。在调用的时候可以传一个位于 0 ~ 1 之间的数字进去。而这个 0 ~ 1 之间的数字就是你想要压缩的图片质量,数值越小,压缩的图片的尺寸也会越小。

然而,实际上呢?

我在 Vue 里实现了一个方法,大概长这样:

Vue.prototype.$comp = function(file, quality) {
  if (file[0]) {
    return Promise.all(Array.from(file).map(e => Vue.prototype.$compression(e, quality))) // 如果是 file 数组返回 Promise 数组
  } else {
    return new Promise((resolve) => {
      const reader = new FileReader() // 创建 FileReader
      reader.onload = ({ target: { result: src }}) => {
        const image = new Image() // 创建 img 元素
        image.onload = async() => {
          const canvas = document.createElement('canvas') // 创建 canvas 元素
          canvas.width = image.width
          canvas.height = image.height
          canvas.getContext('2d').drawImage(image, 0, 0, image.width, image.height) // 绘制 canvas
          const canvasURL = canvas.toDataURL('image/jpeg', quality)
          const buffer = atob(canvasURL.split(',')[1])
          let length = buffer.length
          const bufferArray = new Uint8Array(new ArrayBuffer(length))
          while (length--) {
            bufferArray[length] = buffer.charCodeAt(length)
          }
          const miniFile = new File([bufferArray], file.name, { type: 'image/jpeg' })
          resolve({
            file: miniFile,
            origin: file,
            beforeSrc: src,
            afterSrc: canvasURL,
            beforeKB: Number((file.size / 1024).toFixed(2)),
            afterKB: Number((miniFile.size / 1024).toFixed(2))
          })
        }
        image.src = src
      }
      reader.readAsDataURL(file)
    })
  }
}

大家可别小瞧了这个代码,这可是可以拿优秀员工的代码的!!保存!!!


同时这里有一张网图:


此时在组件中调用压缩方法:

renderImg(files) { // 获取文件
      console.log(files[0])
      this.$comp(files[0], 0.9).then(res => console.log(res.file))
}


打印结果为:

是的!没错!压缩后的图片体积变大了,情况发生在当 quality 区间在 0.5 ~ 1 时,不同图片有所不同。

当然也有些图片压缩后的体积都是小于原体积的:

renderImg(files) { // 获取文件
      console.log(files[0])
      this.$comp(files[0], 0.999999).then(res => console.log(res.file))
}


结果是:

 

所以现在的结论是:

  • 第一:canvas.toDataURL/toBlob 这两个 API确实可以实现图片压缩,但是压缩后的体积是不可控的,可能在一定的 quality 值外,图片反而会变大。
  • 第二:quality 的大小和压缩后的图片体积没有一个通用的曲线函数可以描述。

第一点上面已经验证过了,下面来验证第二点:

下面会分别用几张不同大小的图片,分别从 quality 0.01 一直压缩到 0.99 ,已验证 quality 和压缩后的图片的关系:

首先是一张:


压缩曲线图为:

接下来是一张:


压缩曲线图为:


接下来是一张:

 
压缩曲线图为:


最后是一张:

 
压缩曲线图为:

以上曲线图,横轴是 quality,纵轴是当前 quality 压缩图片后图片的体积(单位:byte)。

虽然可以确定,没有一个常规曲线函数可以描述 quality 和压缩后图片体积的关系。但是从总体趋势来说,压缩后的图片体积和 quality 是成正比的。

而在实际的项目应用中,我们关心比较多的也是:如何把大图压缩到指定体积大小就好。而不是把图片压缩至自身的 1/2 或者 1/3 这样的。

所以,即使是压缩后的图片可能会大于原图。我们也不会太关心。我们只需要找到能让图片压缩到指定大小的 quality 就好。

那么怎么找呢?

鉴于上面的曲线图,我们没有办法根据压缩后的体积或 quality 反推出彼此。最笨的办法是像我实现这个曲线图一样,把 quality 从 0.01 一直试到 0.99。但是这样下来,可能需要 10s 左右。是的!十秒!执行一百次左右的压缩需要10s左右。单次压缩需要的耗时大约在 100ms ~ 130 ms 左右。当然这个很图片大小以及 quality 有关。

那么有没有一种快速的方式找到指定的 quality 呢?

使用二分法。

简单来说,就是我有一个 200KB 的图片,我想要压缩到 100KB。但是我不知道 quality 为多少时图片为 100KB。但是我知道 quality 越大图片就越大。因此,我先让 quality 为 0.5。此时压缩一次图片,并得到压缩后的体积。和 100KB 进行比较,如果大于 100KB ,表示 quality 大于 0.5,如果小于 100KB,表示 quality 小于 0.5。假设现在 quality 大于 0.5。那么就让 quality 变为 0.75。根据得出的结果判断 quality 是在 0.5~0.75之间,还是0.75~0.99之间。得到结果之后再让将 quality 设置为 0.625 或者 0.875...

以此类推,quality 的大小就能被定位在 1 / 2 的 n 次方这个范围。n 即二分的次数。

有了这个思路,我们做起来就很快了:

Vue.prototype.$compression = function(file, size = 20, device = 4) {
  if (file[0]) {
    return Promise.all(Array.from(file).map(e => Vue.prototype.$compression(e, size))) // 如果是 file 数组返回 Promise 数组
  } else {
    return new Promise((resolve) => {
      const reader = new FileReader() // 创建 FileReader
      reader.onload = ({ target: { result: src }}) => {
        const fileSize = Number((file.size / 1024).toFixed(2))
        if (fileSize <= size) {
          resolve({ file: file, origin: file, beforeSrc: src, afterSrc: src, beforeKB: fileSize + 'KB', afterKB: fileSize + 'KB', detail: [], quality: null })
        } else {
          const image = new Image() // 创建 img 元素
          image.onload = async() => {
            const canvas = document.createElement('canvas') // 创建 canvas 元素
            canvas.width = image.width
            canvas.height = image.height
            canvas.getContext('2d').drawImage(image, 0, 0, image.width, image.height) // 绘制 canvas
            let canvasURL, miniFile
            let L = true
            let quality = 0
            const detail = []
            let start = Date.now()
            for (let i = 1; i <= device; i++) {
              canvasURL = canvas.toDataURL('image/jpeg', L ? (quality += 1 / (2 ** i)) : (quality -= 1 / (2 ** i)))
              const buffer = atob(canvasURL.split(',')[1])
              let length = buffer.length
              const bufferArray = new Uint8Array(new ArrayBuffer(length))
              while (length--) {
                bufferArray[length] = buffer.charCodeAt(length)
              }
              miniFile = new File([bufferArray], file.name, { type: 'image/jpeg' });
              (miniFile.size / 1024) > size ? L = false : L = true
              detail.push({
                quality,
                size: miniFile.size,
                kb: Number((miniFile.size / 1024).toFixed(2)),
                time: Date.now() - start
              })
              start = Date.now()
            }
            resolve({
              detail,
              quality,
              file: miniFile,
              origin: file,
              beforeSrc: src,
              afterSrc: canvasURL,
              beforeKB: Number((file.size / 1024).toFixed(2)),
              afterKB: Number((miniFile.size / 1024).toFixed(2))
            })
          }
          image.src = src
        }
      }
      reader.readAsDataURL(file)
    })
  }
}


这就是使用二分法将图片压缩至指定体积的方法。二分的次数默认为 4 次。执行时长为 400ms ~ 500ms。还算可以接受,不过因为二分精度的原因,不能保证图片一定会精准的压缩至指定体积,可能会有一定误差。如果要适当提高精度,可以增加二分次数,但是这又会使得图片压缩时间过长。我在上面的方法返回的对象里,记录了每次压缩的效果的时长,这里有个简单的例子:


上面的压缩结果还算符合预期。再来一个:

 


这个就有点偏差了,改下二分次数试试:


将 4 次二分改为 6 次。实际压缩结果更接近期望结果。但是执行时长却多了 100多 ms。

DEMO体验地址
以上就是本人在后台管理系统项目中遇到的图片压缩问题以及解决方案。虽然有些粗糙,但是勉强能实现需求。当然这只是后台管理系统的实现,仅适用于P端。如果是移动端,还要注意处理一些边界情况,如 IOS 尺寸限制,png 透明图变黑等。

码字不易,希望多多支持。如果你有什么更好的想法或者发现了BUG,还请不吝赐教,万分感谢。


————————————————
版权声明:本文为CSDN博主「树林同学」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_34183908/article/details/112491598

前端图片最优化压缩方案 - 掘金前端最优化压缩图片上传方案!!!再次优化升级!14M压缩后仅剩130KB。 图片上传是前端中常见的的业务场景。无论是前台还是后台,适当的对图片进行压缩处理, 可以显著的提升用户体验。https://juejin.cn/post/6940430496128040967

 

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <input id="fileInput" type="file" />
  <img id="img" src="" alt="">

</body>
<script>
  let fileId = document.getElementById('fileInput')
  let img = document.getElementById('img')
  fileId.onchange = function (e) {
    let file = e.target.files[0]
    compressImg(file, 0.92).then(res => {//compressImg方法见附录
      console.log(res)
      img.src = window.URL.createObjectURL(res.file);
    })
  }
  /**
   * 压缩方法 
   * @param {string} file 文件
   * @param {Number} quality  0~1之间
  */
  function compressImg(file, quality) {
    if (file[0]) {
      return Promise.all(Array.from(file).map(e => compressImg(e,
        quality))) // 如果是 file 数组返回 Promise 数组
    } else {
      return new Promise((resolve) => {
        const reader = new FileReader() // 创建 FileReader
        reader.onload = ({
          target: {
            result: src
          }
        }) => {
          const image = new Image() // 创建 img 元素
          image.onload = async () => {
            const canvas = document.createElement('canvas') // 创建 canvas 元素
            canvas.width = image.width
            canvas.height = image.height
            canvas.getContext('2d').drawImage(image, 0, 0, image.width, image.height) // 绘制 canvas
            const canvasURL = canvas.toDataURL('image/jpeg', quality)
            const buffer = atob(canvasURL.split(',')[1])
            let length = buffer.length
            const bufferArray = new Uint8Array(new ArrayBuffer(length))
            while (length--) {
              bufferArray[length] = buffer.charCodeAt(length)
            }
            const miniFile = new File([bufferArray], file.name, {
              type: 'image/jpeg'
            })
            resolve({
              file: miniFile,
              origin: file,
              beforeSrc: src,
              afterSrc: canvasURL,
              beforeKB: Number((file.size / 1024).toFixed(2)),
              afterKB: Number((miniFile.size / 1024).toFixed(2))
            })
          }
          image.src = src
        }
        reader.readAsDataURL(file)
      })
    }
  }

</script>

</html>

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

canvas节点无法导出图片_前端实现图片压缩及遇到的问题 的相关文章

  • 使用NGINX作为HTTPS正向代理服务器

    简介 xff1a NGINX主要设计作为反向代理服务器 xff0c 但随着NGINX的发展 xff0c 它同样能作为正向代理的选项之一 正向代理本身并不复杂 xff0c 而如何代理加密的HTTPS流量是正向代理需要解决的主要问题 本文将介绍
  • 超级小的docker镜像

    超级小的docker镜像 xff0c 通过可用来做基础镜像或者测试使用 拉镜像尽量选择带有apline或slim标签的 xff0c 体积很小 alpine镜像官方地址 xff1a https hub docker com alpine 拉取
  • 16进制转Base64的实现原理及代码

    随着计算机技术的发展 xff0c 数据的存储和传输方式也在不断更新 xff0c 其中十六进制字符串和Base64编码是两种常见的数据表示方式 本文将介绍16进制字符串和Base64编码的原理 xff0c 并提供Java代码实现16进制字符串
  • MySQL错误-this is incompatible with sql_mode=only_full_group_by解决办法

    原因分析 xff1a 一 原理层面 这个错误发生在mysql 5 7 5 版本及以上版本会出现的问题 xff1a mysql 5 7 5版本以上默认的sql配置是 sql mode 61 ONLY FULL GROUP BY xff0c 这
  • pulsar-admin接入项目

    练手项目 第一步 xff1a 添加依赖 lt dependency gt lt groupId gt org apache pulsar lt groupId gt lt artifactId gt pulsar client admin
  • C/C++语言——函数返回临时变量、对象

    函数内部栈上创建的临时变量 临时对象 xff0c 生命周期只在函数运行期间 xff0c 函数运行结束后 xff0c 就会释放对应内存空间 错误的函数写法 int amp test1 int x 61 1 return x 错误的函数写法 i
  • VS2008——调试方法大全

    一 断点调试 总结以下VS2008的调试方法 xff0c 首先最常用的就是使用断点了 xff0c 断点分为两种 xff1a 普通断点 跟踪点 普通断点就是红色圆点 xff0c 跟踪点是红色菱形 可以通过右键设置断点相关内容 xff0c 让断
  • Effective C++——22.将成员变量声明为private

    1 一致性 xff1a 如果public中的每样东西都是函数 xff0c 就不需要思考使用的时候要不要带小括号 2 使用函数可以让成员变量的处理有更精确的控制 3 封装性 xff1a 有时候要根据不同情况修改set和get的实现方式 xff
  • Effective C++——4.确定对象被使用前已经初始化

    1 为防止有的情况下对象未初始化导致的混乱 xff0c 最佳的处理办法就是 xff1a 永远在使用对象之前先将它初始化 对于无任何成员的内置类型 xff0c 必须手工完成 2 内置类型以外的任何其他东西 xff0c 初始化责任在构造函数中
  • Effective C++——32.确定public继承是“is-a”关系

    1 is a关系的概念 xff0c 就是基类可以实现的事情 xff0c 子类就一定要能实现 如果不能实现 xff0c 可以修改设计 xff0c 比如将基类能实现 xff0c 子类却不能实现的事情派生出一个中间基类 记住一点 xff1a 1
  • Java中repaint方法清除原来图像问题

    虽然Java界面编程作用不大 xff0c 但在兴趣的驱使下还是了解了一下 xff0c 在写小程序的时候发现了repaint方法有时候会清理原来的图像 xff0c 有时候又不清理 下面贴出我通过API文档得出的结论 xff0c 以下例子窗口为
  • 怎样找回自己CSDN丢失博客?

    写博客是件很欢快的事情 xff0c 但没多久就发现自己刚写的博客丢失 xff0c 这是一件更加 欢快 的事情 百度了一下关于CSDN博客丢失的现象 xff0c 虽然不算很多 xff0c 但还是有个别人也同样发生了这样的情况 注 xff1a
  • 拓扑排序(队列实现)

    什么是拓扑排序呢 xff1f 就是将一个有向无环图中所有顶点在不违反先决条件关系的前提下排成线性序列的过程称为拓扑排序 学拓扑排序有什么用呢 xff1f 当然有用啦 比如说学校排课的时候 xff0c 会考虑到有的课程需要先修 我们学完C程序
  • Arch的VBox安装笔记

    Arch Version archlinux 2015 12 01 dual Virtual Box Version 5 0 16 r105871 Update xff1a 2016 03 26 17 08 18 1 分区 xff08 cf
  • Java Runtime (class file version 61.0), this version of the Java Runtime only recognizes class file

    maven打包报错 org springframework boot maven BuildInfoMojo hasbeen copiled by a more recent version of the Java Runtime clas
  • 前端JS接收服务端的二进制文件流实现文件下载

    前端JS接收服务端的二进制文件流实现文件下载 var binaryData 61 binaryData push res 改成Boole或者file类型 const url 61 window URL createObjectURL new
  • debian10ssh配置用户限制,日志等

    需求 xff1a 工作端口为2021 xff1b 只允许用户user01 xff0c 密码ChinaSkill21登录到router 其他用户 xff08 包括root xff09 不能登录 xff0c 创建一个新用户 xff0c 新用户可
  • CentOS搭建PySpider爬虫服务

    1 环境准备 前置环境部署 在开始部署前 xff0c 我们需要做一些前置准备 yum 更新 yum span class hljs operator span class hljs keyword update span y span 安装
  • openGauss5.0企业版CentOS单节点安装

    目录 一 安装环境 二 前置依赖包 三 安装前准备1 四 安装前准备2 五 安装 一 安装环境 CPU xff1a 2核 内存 xff1a 4G 磁盘 xff1a 20G 操作系统 xff1a CentOS 7 9 python版本 xff
  • SpringBoot+PageHelper+Bootstrap+Thymeleaf 实现分页功能

    本文针对那种想要快速实现功能 xff0c 而不是研究原理的 xff0c 那你就直接复制我的东西 xff0c 运行就好 如果想深入学习的同学请另行百度 第一种 Spring Boot 43 Thymeleaf 使用PageHelper实现分页

随机推荐

  • Idea集成使用SVN教程

    idea 从项目窗口跳到打开项目选项窗口 操作之后即可跳到如下界面 第一步 下载svn的客户端 xff0c 通俗一点来说就是小乌龟啦 xff01 官网下载地址 xff1a Downloads TortoiseSVN 下载之后直接安装就好了
  • IntelliJ IDEA 下的svn配置及使用的非常详细的图文总结

    IntelliJ IDEA 下的svn配置及使用的非常详细的图文总结 请叫我大师兄 CSDN博客 ideasvn配置 https blog csdn net qq 27093465 article details 74898489 首先 x
  • webpack 错误

    1 ERROR in src main css Module build failed from node modules mini css extract plugin dist loader js ReferenceError docu
  • 客户端js 读取 json 数据

    采用 XMLHttpRequest 读取 1 new 初始化 XMLHttpRequest 2 open 设置请求方式 xff0c 地址 3 send 发起请求 4 onload 请求成功 xff0c 返回结果 代码 xff1a lt DO
  • dataTable的中文文档

    DataTables Table plug in for jQuery https datatables net 用google 打开 xff0c 直接翻译 参考 选项 DataTables 及其扩展是极其可配置的库 xff0c 它们对 H
  • nodejs核心API

    1 Buffer对象 不需要引入 Bufferd对象用途 以二进制流进行数据的传送传递 1 三种创建方式 类似于数组的创建 用16进制存储 let buf1 61 Buffer alloc 20 13 console log buf1 lt
  • mysql select, from ,join ,on ,where groupby,having ,order by limit的执行顺序和书写顺序

    mysql select xff0c from xff0c join xff0c on xff0c where groupby having order by limit的执行顺序和书写顺序 关键字或 解释执行顺序select 查询列表 x
  • jQuery对checkbox的各种操作

    注意 xff1a 操作checkbox的checked disabled属性时jquery1 6以前版本用attr 1 6以上 xff08 包含 xff09 建议用prop 1 根据id获取checkbox 34 cbCheckbox1 3
  • dom 操作排他思路

    1 遍历所有 xff0c 操作 xff0c 对具体的重新操作 实现点击一个按钮就把背景颜色改为pink颜色 lt DOCTYPE html gt lt html lang 61 34 en 34 gt lt head gt lt meta
  • web前端兼容

    兼容性 xff1a Compatibility overview https www quirksmode org compatibility html 当前测试 CSS所有 CSS 选择器和声明 xff08 最终 xff09 DOM所有
  • 使用跨域资源共享的 DOM 访问控制

    Dev Opera DOM Access Control Using Cross Origin Resource Sharing https dev opera com articles dom access control using c
  • react-typescript 错误

    64 types jsurl index d ts 39 is not a module npm i jsurl 安装错误 xff0c 因为 typescript无法用此方法安装 xff0c 卸载干净 1 增加types目录 新建jsurl
  • react-router-dom错误

    index tsx 24 Uncaught Error useLocation may be used only in the context of a lt Router gt component at invariant index t
  • Total Blocking Time 总阻塞时间 (TBT)

    总阻塞时间 TBT 是测量加载响应度的重要实验室指标 xff0c 因为该项指标有助于量化在页面交互性变为可靠前 xff0c 不可交互程度的严重性 xff0c 较低的 TBT 有助于确保页面的可用性 什么是 TBT xff1f 总阻塞时间 T
  • JavaScript中的回调的作用是什么

    这期内容当中小编将会给大家带来有关JavaScript中的回调的作用是什么 xff0c 文章内容丰富且以专业的角度为大家分析和叙述 xff0c 阅读完这篇文章希望大家可以有所收获 回调函数 首先写一个向人打招呼的函数 只需要创建一个接受 n
  • css如何换行

    在css中通过word break与white space这两个属性来设置自动换行 xff0c 其中word wrap属性允许长单词或URL地址换行到下一行 xff1b 而white space属性可以设置文本换行方式 本文操作环境 xff
  • JS 实现复制功能(document.execCommand)

    功能 xff1a 点击按钮 xff0c 复制值 实现方法 xff1a 通过原生js 的方法document execCommand 39 copy 39 坑 xff1a document execCommand copy 不生效 不能实现的
  • Linux chmod命令 修改文件权限被禁止(not permitted)的解决办法

    解决方法 在Linux环境下 xff0c 修改文件时以外导致文件没有权限读取和修改 xff0c 在修改相关文件 usr bin docker的属性的时 chmod 777 usr bin containerd chmod changing
  • springsource-tools下载安装

    下载springsource tools我弄了一个多小时才找到下载地址 xff0c 必须得好好记录一下 首先 需要避免一个误区 xff1a 下载的不是spring tools 这个下载后是一个jar包 也不是一个可执行文件的压缩包 xff0
  • canvas节点无法导出图片_前端实现图片压缩及遇到的问题

    图片上传是前端中常见的的业务场景 无论是前台还是后台 xff0c 适当的对图片进行压缩处理 xff0c 可以显著的提升用户体验 而在后台管理系统中 xff0c 图片压缩不仅仅能够提升后台管理员操作体验 xff0c 更是可以防止后台设置过大的