如何实现图片压缩上传

2019 Java 开发者跳槽指南.pdf (吐血整理)….>>>

背景

实际生产中经常遇到这样的场景:为减小服务器压力,上传附件尤其是图片的时候,往往需要限制上传文件的大小。而限制的方案也有两种,一种就是限制用户可上传的文件大小,由用户来选择上传的文件和如果文件过大由用户自行进行压缩裁剪;另一种就是由服务进行图片的压缩和大小控制然后再上传到服务器。这里主要介绍的是第二种方案。

主要技术

前边有介绍过证书的生成和下载,其中就有证书的压缩和打包的相关操作,感兴趣的可以看下本人的那篇文章。这里同样是采用的该原理,步骤如下:

关键步骤

图片文件-->文件流(base64位编码)-->canvas-->压缩-->生成压缩后的文件-->上传。

这里的压缩过程,做了相应的优化。优化方案有两种,一种是重复压缩,一种是计算比例压缩。

而由于压缩比和文件大小并不是正比例关系,所有可以保险起见再乘以一个系数。比如:quality: 1024*0.7/fileObj.size(0.7是保险系数,1024是限制大小1M的意思,可根据个人需要自行调整参数,也可以封装成接口参数统一修改)

这里还自行封装了一个进度组件,使用的是原生js。

代码

代码和相关注释如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>文件压缩上传</title>
    <script type="text/javascript">
        /*
        三个参数
        file:一个是文件(类型是图片格式),
        w:一个是文件压缩的后宽度,宽度越小,字节越小
        objDivOrCallback:一个是容器或者回调函数
        photoCompress()
         */
        function photoCompress(file,w,objDivOrCallback) {
            var ready = new FileReader()
            /*开始读取指定的Blob对象或File对象中的内容. 当读取操作完成时,readyState属性的值会成为DONE,如果设置了onloadend事件处理程序,则调用之.同时,result属性中将包含一个data: URL格式的字符串以表示所读取文件的内容.*/
            ready.readAsDataURL(file)
            ready.onload = function() {
                var re = this.result
                canvasDataURL(re, w, objDivOrCallback)
            }
        }
        function canvasDataURL(path, obj, callback) {
            var img = new Image()
            img.src = path
            img.onload = function(){
                var that = this
                // 默认按比例压缩
                var w = that.width,
                    h = that.height,
                    scale = w / h
                w = obj.width || w
                h = obj.height || (w / scale)
                var quality = 0.7  // 默认图片质量为0.7
                //生成canvas
                var canvas = document.createElement('canvas')
                var ctx = canvas.getContext('2d')
                // 创建属性节点
                var anw = document.createAttribute("width")
                anw.nodeValue = w
                var anh = document.createAttribute("height")
                anh.nodeValue = h
                canvas.setAttributeNode(anw)
                canvas.setAttributeNode(anh)
                ctx.drawImage(that, 0, 0, w, h)
                // 图像质量
                if(obj.quality && obj.quality <= 1 && obj.quality > 0) {
                    quality = obj.quality
                }
                // quality值越小,所绘制出的图像越模糊
                var base64 = canvas.toDataURL('image/jpeg', quality)
                // 这里不能直接quality: 0.2,因为这样就相当于还是在原来的大小的基础上压缩
                var bl = convertBase64UrlToBlob(base64)
                // 如果还大于1M,继续压缩--代码待优化,可以减去重复生成文件和转码的过程
                if (bl.size/1024 > 1025) {
                    // 其实也可以在这里直接写一个匹配压缩比直到大小小于1的方法
                    photoCompress(bl, {
                    quality: 0.5 * obj.quality
                  }, callback)
                } else {
                    callback(bl)
                }
                // 回调函数返回base64的值--改为返回文件对象
                // callback(base64)
            }
        }
        /**
         * 将以base64的图片url数据转换为Blob
         * @param urlData
         *            用url方式表示的base64图片数据
         */
        function convertBase64UrlToBlob(urlData) {
            var arr = urlData.split(','), mime = arr[0].match(/:(.*?);/)[1],
                bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n)
            while(n--) {
                u8arr[n] = bstr.charCodeAt(n)
            }
            return new Blob([u8arr], {type:mime})
        }
 
 
        var xhr
        //上传文件方法
        function UpladFile() {
            var fileObj = document.getElementById("file").files[0] // js 获取文件对象
            var url = "http://pxjy.api.test.nercel.cn/file/publicFile/upload" // 接收上传文件的后台地址 
 
            var form = new FormData() // FormData 对象
 
            if(fileObj.size/1024 > 1025) { //大于1M,进行压缩上传
                photoCompress(fileObj, {
                    // 这里还有一种方案,那就是这里的quality改为计算压缩比(由于压缩比和文件大小并不是正比例关系,所有可以保险起见再乘以一个系数)
                    // 压缩比计算的方案:quality: 1024*0.7/fileObj.size--0.7是保险系数--这些参数可以进一步封装
                    quality: 0.2
                // }, function(base64Codes){
                // 修改为返回文件对象
                }, function(bl){
                    //console.log("压缩后:" + base.length / 1024 + " " + base);
                    // var bl = convertBase64UrlToBlob(base64Codes)
                    // form.append("file", bl, "file_"+Date.parse(new Date())+".jpg"); // 文件对象
                    form.append("multipartFile", bl, "file_"+Date.parse(new Date())+".jpg") // 文件对象
                    xhr = new XMLHttpRequest()  // XMLHttpRequest 对象
                    xhr.open("post", url, true) //post方式,url为服务器请求地址,true 该参数规定请求是否异步处理。
                    xhr.setRequestHeader("enctype", "multipart/form-data") // 设置请求头
                    xhr.setRequestHeader("Authorization", "Bearer 8d782bb1-768f-4fa7-80d2-5e2b6d6a6f64") // 设置请求头
                    // open后才可以设置头
                    xhr.onload = uploadComplete //请求完成
                    xhr.onerror =  uploadFailed //请求失败
 
                    xhr.upload.onprogress = progressFunction//【上传进度调用方法实现】
                    xhr.upload.onloadstart = function(){//上传开始执行方法
                        ot = new Date().getTime()   //设置上传开始时间
                        oloaded = 0//设置上传开始时,以上传的文件大小为0
                    };
 
                    xhr.send(form) //开始上传,发送form数据
                })
            } else { //小于等于1M 原图上传
                // form.append("file", fileObj) // 文件对象
                form.append("multipartFile", fileObj) // 文件对象
                xhr = new XMLHttpRequest()  // XMLHttpRequest 对象
                xhr.open("post", url, true) //post方式,url为服务器请求地址,true 该参数规定请求是否异步处理。
                xhr.setRequestHeader("enctype", "multipart/form-data") // 设置请求头
                xhr.setRequestHeader("Authorization", "Bearer 8d782bb1-768f-4fa7-80d2-5e2b6d6a6f64") // 设置请求头
                    // open后才可以设置头
                xhr.onload = uploadComplete //请求完成
                xhr.onerror =  uploadFailed //请求失败
 
                xhr.upload.onprogress = progressFunction//【上传进度调用方法实现】
                xhr.upload.onloadstart = function() {//上传开始执行方法
                    ot = new Date().getTime()   //设置上传开始时间
                    oloaded = 0//设置上传开始时,以上传的文件大小为0
                }
 
                xhr.send(form) //开始上传,发送form数据
            }
        }
 
        //上传成功响应
        function uploadComplete(evt) {
            //服务断接收完文件返回的结果
            var data = JSON.parse(evt.target.responseText)
            if(data.code === 200) {
                uploadSuccess()
            } else {
                uploadFailed()
            }
 
        }
        //上传失败
        function uploadFailed(evt) {
            alert("上传失败!")
        }
        //上传成功
        function uploadSuccess(evt) {
            alert("上传成功!")
        }
        //取消上传
        function cancleUploadFile(){
            xhr.abort()
        }
 
        //上传进度实现方法,上传过程中会频繁调用该方法
        function progressFunction(evt) {
            var progressBar = document.getElementById("progressBar")
            var percentageDiv = document.getElementById("percentage")
            // event.total是需要传输的总字节,event.loaded是已经传输的字节。如果event.lengthComputable不为真,则event.total等于0
            if (evt.lengthComputable) {//
                progressBar.max = evt.total
                progressBar.value = evt.loaded
                percentageDiv.innerHTML = Math.round(evt.loaded / evt.total * 100) + "%"
            }
            var time = document.getElementById("time")
            var nt = new Date().getTime()//获取当前时间
            var pertime = (nt-ot)/1000 //计算出上次调用该方法时到现在的时间差,单位为s
            ot = new Date().getTime() //重新赋值时间,用于下次计算
            var perload = evt.loaded - oloaded //计算该分段上传的文件大小,单位b
            oloaded = evt.loaded//重新赋值已上传文件大小,用以下次计算
            //上传速度计算
            var speed = perload/pertime//单位b/s
            var bspeed = speed
            var units = 'b/s'//单位名称
            if(speed/1024>1) {
                speed = speed/1024
                units = 'k/s'
            }
            if(speed/1024>1) {
                speed = speed/1024
                units = 'M/s'
            }
            speed = speed.toFixed(1)
            //剩余时间
            var resttime = ((evt.total-evt.loaded)/bspeed).toFixed(1)
            time.innerHTML = ',速度:'+speed+units+',剩余时间:'+resttime+'s'
            if(bspeed==0) time.innerHTML = '上传已取消'
        }
    </script>
</head>
<body>
<progress id="progressBar" value="0" max="100" style="width: 300px;"></progress>
<span id="percentage"></span><span id="time"></span>
<br /><br />
<input type="file" id="file" name="myfile" accept="image/x-png, image/jpg, image/jpeg, image/gif"/>
<input type="button" onclick="UpladFile()" value="上传" />
<input type="button" onclick="cancleUploadFile()" value="取消" />
</body>
</html>

此处是借鉴网上思路的基础上的个人修改完善后的代码, 并且有待有时间的时候做进一步封装优化和封装成npm组件以及vue组件。

代码git地址:

https://github.com/MRlijiawei/components/blob/master/file/%E5%9B%BE%E7%89%87%E5%8E%8B%E7%BC%A9%E4%B8%8A%E4%BC%A0.html

 

扩展

 

  png图片的另一种压缩方案

png的简介

什么是png:

PNG的全称叫便携式网络图型(Portable Network Graphics)是目前最流行的网络传输和展示的图片格式,原因有如下几点:

  • 无损压缩:PNG图片采取了基于LZ77派生算法对文件进行压缩,使得它压缩比率更高,生成的文件体积更小,并且不损失数据。
  • 体积小:它利用特殊的编码方法标记重复出现的数据,使得同样格式的图片,PNG图片文件的体积更小。网络通讯中因受带宽制约,在保证图片清晰、逼真的前提下,优先选择PNG格式的图片。
  • 支持透明效果:PNG支持对原图像定义256个透明层次,使得图像的边缘能与任何背景平滑融合,这种功能是GIF和JPEG没有的。

当初就是因为png的透明特性才开始喜欢它的。

png的类型:

  • PNG 8:PNG 8中的8,其实指的是8bits,相当于用2^8(2的8次方)大小来存储一张图片的颜色种类,2^8等于256,也就是说PNG 8能存储256种颜色,一张图片如果颜色种类很少,将它设置成PNG 8得图片类型是非常适合的。
  • PNG 24:PNG 24中的24,相当于3乘以8 等于 24,就是用三个8bits分别去表示 R(红)、G(绿)、B(蓝)。R(0~255),G(0~255),B(0~255),可以表达256乘以256乘以256=16777216种颜色的图片,这样PNG 24就能比PNG 8表示色彩更丰富的图片。但是所占用的空间相对就更大了。
  • PNG 32:PNG 32中的32,相当于PNG 24 加上 8bits的透明颜色通道,就相当于R(红)、G(绿)、B(蓝)、A(透明)。R(0~255),G(0~255),B(0~255),A(0~255)。比PNG 24多了一个A(透明),也就是说PNG 32能表示跟PNG 24一样多的色彩,并且还支持256种透明的颜色,能表示更加丰富的图片颜色类型。

png图片的数据编码:

PNG图片的数据结构其实跟http请求的结构很像,都是一个数据头,后面跟着很多的数据块,如下图所示:

使用16进制编码打开png图片,部分编码示例如下:

8950 4e47 0d0a 1a0a:这个是PNG图片的头,所有的PNG图片的头都是这一串编码,图片软件通过这串编码判定这个文件是不是PNG格式的图片。

0000 000d:是iHDR数据块的长度,为13。

4948 4452:是数据块的type,为IHDR,之后紧跟着是data。

0000 0292:是图片的宽度。

0000 024e:是高度。

以此类推,每一段十六进制编码就代表着一个特定的含义。感兴趣的可以自行百度。

所以,颜色重复度越大的、越接近的(渐变的颜色或透明度等),编码重复度就越大,就越容易压缩。

压缩原理:

png图片用差分编码(Delta encoding)对图片进行预处理,处理每一个的像素点中每条通道的值。

压缩阶段会将预处理阶段得到的结果进行Deflate压缩,它由 Huffman 编码 和 LZ77压缩构成。

压缩后的结果就是一串处理后的编码,保存到数据库中,占用空间会小很多,在使用的时候,再进行逆向解析渲染。

具体代码暂无。