在做移动端图片上传的时候,用户传的都是手机本地图片,而本地图片一般都相对比较大,拿iphone6来说,平时拍很多图片都是一两M的,如果直接这样上传,那图片就太大了,如果用户用的是移动流量,完全把图片上传显然不是一个好办法。
目前来说,HTML5的各种新API都在移动端的webkit上得到了较好的实现。根据查看caniuse,本demo里使用到的FileReader、Blob、Formdata对象均已在大部分移动设备浏览器中得到了实现(safari6.0+、android 3.0+),所以直接在前端压缩图片,已经成了很多移动端图片上传的必备功能了。
在移动端压缩图片并且上传主要用到filereader、canvas 以及 formdata 这三个h5的api。逻辑并不难。整个过程就是:
(1)用户使用input file上传图片的时候,用filereader读取用户上传的图片数据(base64格式)
(2)把图片数据传入img对象,然后将img绘制到canvas上,再调用canvas.toDataURL对图片进行压缩
(3)获取到压缩后的base64格式图片数据,转成二进制塞入formdata,再通过XmlHttpRequest提交formdata。
说起来好像挺简单,其实还是有些坑的。接下来就直接用代码进行分析:
【一】获取图片数据
先是获取图片数据,也就是监听input file的change事件,然后获取到上传的文件对象files,将类数组的files转成数组,然后进行forEach遍历。
接着判断文件类型,如果不是图片则不作处理。如果是图片就实例化一个filereader,以base64格式读取上传的文件数据,判断数据长度,如果大于200KB的图片就调用compress方法进行压缩,否则调用upload方法进行上传。
filechooser.onchange = function () { if (!this.files.length) return; var files = Array.prototype.slice.call(this.files); if (files.length > 9) { alert("最多同时只可上传9张图片"); ; } files.forEach( (file,i) { if (!/\/(?:jpeg|png|gif)/i.test(file.type)) ; var reader = new FileReader(); var li = document.createElement("li"); li.innerHTML = '<div class="progress"><span></span></div>'; $(".img-list").append($(li)); reader.onload = () { var result = .result; var img = Image(); img.src = result; //如果图片大小小于200kb,则直接上传 if (result.length <= maxsize) { $(li).css("background-image","url(" + result + ")"); img = null; upload(result,file.type,$(li)); ; } 图片加载完毕之后进行压缩,然后上传 if (img.complete) { callback(); } else { img.onload = callback; } callback() { var data = compress(img); $(li).css("background-image","url(" + data + ")"); upload(data,$(li)); img = ; } }; reader.readAsDataURL(file); }) };
【2】压缩图片
上面做完图片数据的获取后,就可以做compress压缩图片的方法了。而压缩图片也并不是直接把图片绘制到canvas再调用一下toDataURL就行的。
在IOS中,canvas绘制图片是有两个限制的:
首先是图片的大小,如果图片的大小超过两百万像素,图片也是无法绘制到canvas上的,调用drawImage的时候不会报错,但是你用toDataURL获取图片数据的时候获取到的是空的图片数据。
再者就是canvas的大小有限制,如果canvas的大小大于大概五百万像素(即宽高乘积)的时候,不仅图片画不出来,其他什么东西也都是画不出来的。
应对第一种限制,处理办法就是瓦片绘制了。瓦片绘制,也就是将图片分割成多块绘制到canvas上,我代码里的做法是把图片分割成100万像素一块的大小,再绘制到canvas上。
而应对第二种限制,我的处理办法是对图片的宽高进行适当压缩,我代码里为了保险起见,设的上限是四百万像素,如果图片大于四百万像素就压缩到小于四百万像素。四百万像素的图片应该够了,算起来宽高都有2000X2000了。
如此一来就解决了IOS上的两种限制了。
除了上面所述的限制,还有两个坑,一个就是canvas的toDataURL是只能压缩jpg的,当用户上传的图片是png的话,就需要转成jpg,也就是统一用canvas.toDataURL('image/jpeg',0.1) , 类型统一设成jpeg,而压缩比就自己控制了。
另一个就是如果是png转jpg,绘制到canvas上的时候,canvas存在透明区域的话,当转成jpg的时候透明区域会变成黑色,因为canvas的透明像素默认为rgba(0,0),所以转成jpg就变成rgba(0,1)了,也就是透明背景会变成了黑色。解决办法就是绘制之前在canvas上铺一层白色的底色。
compress(img) { var initSize = img.src.length; var width = img.width; var height = img.height; 如果图片大于四百万像素,计算压缩比并将大小压至400万以下 var ratio; if ((ratio = width * height / 4000000)>1) { ratio = Math.sqrt(ratio); width /= ratio; height /= ratio; } { ratio = 1; } canvas.width = width; canvas.height = height; 铺底色 ctx.fillStyle = "#fff"; ctx.fillRect(0,0,canvas.width,canvas.height); 如果图片像素大于100万则使用瓦片绘制 count; if ((count = width * height / 1000000) > 1) { count = ~~(Math.sqrt(count)+1); 计算要分成多少块瓦片 计算每块瓦片的宽和高 var nw = ~~(width / count); var nh = ~~(height / count); tCanvas.width = nw; tCanvas.height = nh; for (var i = 0; i < count; i++) { var j = 0; j < count; j++) { tctx.drawImage(img,i * nw * ratio,j * nh * ratio,nw * ratio,nh * ratio,nw,nh); ctx.drawImage(tCanvas,i * nw,j * nh,nh); } } } { ctx.drawImage(img,0,width,height); } 进行最小压缩 var ndata = canvas.toDataURL('image/jpeg',0.1); console.log('压缩前:' + initSize); console.log('压缩后:' + ndata.length); console.log('压缩率:' + ~~(100 * (initSize - ndata.length) / initSize) + "%"); tCanvas.width = tCanvas.height = canvas.width = canvas.height = 0 ndata; }
【三】图片上传
完成图片压缩后,就可以塞进formdata里进行上传了,先将base64数据转成字符串,再实例化一个ArrayBuffer,然后将字符串以8位整型的格式传入ArrayBuffer,再通过BlobBuilder或者Blob对象,将8位整型的ArrayBuffer转成二进制对象blob,然后把blob对象append到formdata里,再通过ajax发送给后台即可。
XmlHttpRequest2中不仅可以发送大数据,还多出了比如获取发送进度的API,我代码里也进行了简单的实现。
图片上传,将base64的图片转成二进制对象,塞进formdata上传 upload(basestr,type,$li) { var text = window.atob(basestr.split(",")[1]); var buffer = ArrayBuffer(text.length); var ubuffer = Uint8Array(buffer); var pecent = 0,loop = var i = 0; i < text.length; i++) { ubuffer[i] = text.charCodeAt(i); } var Builder = window.WebKitBlobBuilder || window.MozBlobBuilder; blob; (Builder) { var builder = Builder(); builder.append(buffer); blob = builder.getBlob(type); } { blob = window.Blob([buffer],{type: type}); } var xhr = XMLHttpRequest(); var formdata = FormData(); formdata.append('imagefile'); xhr.onreadystatechange = () { if (xhr.readyState == 4 && xhr.status == 200) { console.log('上传成功:' + xhr.responseText); clearInterval(loop); 当收到该消息时上传完毕 $li.find(".progress span").animate({'width': "100%"},pecent < 95 ? 200 : 0, () { $(this).html("上传成功"); }); $(".pic-list").append('<a href="' + xhr.responseText + '">' + xhr.responseText + '<img src="' + xhr.responseText + '" /></a>') } }; 数据发送进度,前50%展示该进度 xhr.upload.addEventListener('progress',1)"> (e) { if (loop) ; pecent = ~~(100 * e.loaded / e.total) / 2; $li.find(".progress span").css('width',pecent + "%"); if (pecent == 50) { mockProgress(); } },false); 数据后50%用模拟进度 mockProgress() { ; loop = setInterval( () { pecent++; $li.find(".progress span").css('width',1)">); if (pecent == 99) { clearInterval(loop); } },100) } xhr.send(formdata); }
至此,整个上传的前端图片压缩就完成了,因为是用了formdata提交,所以后台接数据的时候就跟普通form表单提交数据一样处理即可。
汇总前端代码:
<!DOCTYPE html> <html lang="en"> <head> <Meta charset="UTF-8"> <Meta content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" name="viewport"> <title>移动端图片压缩上传demo</title> <style> *{margin: 0;padding: 0;} li{list-style-type: none;} a,input{outline: none;-webkit-tap-highlight-color:rgba(0,1)">);} #choose{display: none;} canvas{width: 100%;border: 1px solid #000000;} #upload{display: block;margin: 10px;height: 60px;text-align: center;line-height: 60px;border: 1px solid;border-radius: 5px;cursor: pointer;} .touch{background-color: #ddd;} .img-list{margin: 10px 5px;} .img-list li{position: relative;display: inline-block;width: 100px;height: 100px;margin: 5px 5px 20px 5px;border: 1px solid rgb(100,149,198);background: #fff no-repeat center;background-size: cover;} .progress{position: absolute;width: 100%;height: 20px;line-height: 20px;bottom: 0;left: 0;background-color:rgba(100,198,.5);} .progress span{display: block;width: 0;height: 100%;background-color:rgb(100,198);text-align: center;color: #FFF;font-size: 13px;} .size{position: absolute;width: 100%;height: 15px;line-height: 15px;bottom: -18px;text-align: center;font-size: 13px;color: #666;} .tips{display: block;text-align:center;font-size: 13px;margin: 10px;color: #999;} .pic-list{margin: 10px;line-height: 18px;font-size: 13px;} .pic-list a{display: block;margin: 10px 0;} .pic-list a img{vertical-align: middle;max-width: 30px;max-height: 30px;margin: -4px 0 0 10px;} </style> </head> <body> <input type="file" id="choose" accept="image/*" multiple> <ul class="img-list"></ul> <a id="upload">上传图片</a> <span class="tips">只允许上传jpg、png及gif</span> <div class="pic-list"> 你上传的图片(图片有效期为1分钟): </div> <script src="/public/jquery-2.1.1.min.js"></script> <script> var filechooser = document.getElementById("choose"); 用于压缩图片的canvas var canvas = document.createElement("canvas"var ctx = canvas.getContext('2d' 瓦片canvas var tCanvas = document.createElement("canvas"var tctx = tCanvas.getContext("2d"var maxsize = 100 * 1024; $("#upload").on("click",1)">() { filechooser.click(); }) .on("touchstart",1)">() { $(this).addClass("touch") }) .on("touchend",1)">this).removeClass("touch") }); filechooser.onchange = () { ; .files); ) { alert("最多同时只可上传9张图片"); ; } files.forEach((file,i) { ; FileReader(); ); 获取图片大小 var size = file.size / 1024 > 1024 ? (~~(10 * file.size / 1024 / 1024)) / 10 + "MB" : ~~(file.size / 1024) + "KB"; li.innerHTML = '<div class="progress"><span></span></div><div class="size">' + size + '</div>'; $(".img-list").append($(li)); reader.onload = () { .result; Image(); img.src = result; $(li).css("background-image",1)">); 如果图片大小小于100kb,则直接上传 maxsize) { img = ; upload(result,$(li)); ; } 图片加载完毕之后进行压缩,然后上传 (img.complete) { callback(); } { img.onload = callback; } callback() { compress(img); upload(data,$(li)); img = ; } }; reader.readAsDataURL(file); }) }; 使用canvas对大图片进行压缩 compress(img) { img.src.length; img.width; img.height; 如果图片大于四百万像素,计算压缩比并将大小压至400万以下 ratio; if ((ratio = width * height / 4000000) > 1) { ratio = Math.sqrt(ratio); width /= ratio; height /= ratio; } { ratio = 1; } canvas.width = width; canvas.height = height; 铺底色 ctx.fillStyle = "#fff"; ctx.fillRect(0,canvas.height); 如果图片像素大于100万则使用瓦片绘制 count; ) { count = ~~(Math.sqrt(count) + 1); 计算要分成多少块瓦片 // 计算每块瓦片的宽和高 count); count); tCanvas.width = nw; tCanvas.height = nh; ) { ) { tctx.drawImage(img,nh); ctx.drawImage(tCanvas,nh); } } } { ctx.drawImage(img,height); } 进行最小压缩 ); console.log('压缩前:' + initSize); console.log('压缩后:' + ndata.length); console.log('压缩率:' + ~~(100 * (initSize - ndata.length) / initSize) + "%"); tCanvas.width = tCanvas.height = canvas.width = canvas.height = 0 ndata; } 图片上传,将base64的图片转成二进制对象,塞进formdata上传 ]); Uint8Array(text.length); ) { buffer[i] = text.charCodeAt(i); } var blob = getBlob([buffer],type); XMLHttpRequest(); var formdata = getFormData(); formdata.append('imagefile'); xhr.onreadystatechange = () { var jsonData = JSON.parse(xhr.responseText); var imagedata = jsonData[0] || {}; var text = imagedata.path ? '上传成功' : '上传失败'; console.log(text + ':' + imagedata.path); clearInterval(loop); 当收到该消息时上传完毕 $li.find(".progress span").animate({'width': "100%"},1)">() { $().html(text); }); if (!imagedata.path) ; $(".pic-list").append('<a href="' + imagedata.path + '">' + imagedata.name + '(' + imagedata.size + ')<img src="' + imagedata.path + '" /></a>'); } }; 数据发送进度,前50%展示该进度 xhr.upload.addEventListener('progress',1)">(e) { ; pecent = ~~(100 * e.loaded / e.total) / 2; $li.find(".progress span").css('width',1)">) { mockProgress(); } },1)">); 数据后50%用模拟进度 mockProgress() { ; loop = setInterval(() { pecent++; $li.find(".progress span").css('width',1)">) { clearInterval(loop); } },1)">) } xhr.send(formdata); } /** * 获取blob对象的兼容性写法 * @param buffer * @param format * @returns {*} */ getBlob(buffer,format) { try { return Blob(buffer,{type: format}); } catch (e) { var bb = new (window.BlobBuilder || window.WebKitBlobBuilder || window.MSBlobBuilder); buffer.forEach((buf) { bb.append(buf); }); bb.getBlob(format); } } * * 获取formdata getFormData() { var isNeedShim = ~navigator.userAgent.indexOf('Android') && ~navigator.vendor.indexOf('Google') && !~navigator.userAgent.indexOf('Chrome') && navigator.userAgent.match(/AppleWebKit\/(\d+)/).pop() <= 534return isNeedShim ? new FormDataShim() : FormData() } * * formdata 补丁,给不支持formdata上传blob的android机打补丁 * @constructor FormDataShim() { console.warn('using formdata shim'var o = [],boundary = Array(21).join('-') + (+new Date() * (1e16 * Math.random())).toString(36),oldSend = XMLHttpRequest.prototype.send; this.append = (name,value,filename) { parts.push('--' + boundary + '\r\nContent-Disposition: form-data; name="' + name + '"'if (value instanceof Blob) { parts.push('; filename="' + (filename || 'blob') + '"\r\nContent-Type: ' + value.type + '\r\n\r\n'); parts.push(value); } { parts.push('\r\n\r\n' + value); } parts.push('\r\n'); }; Override XHR send() XMLHttpRequest.prototype.send = (val) { fr,data,oXHR = if (val === o) { Append the final boundary string parts.push('--' + boundary + '--\r\n' Create the blob data = getBlob(parts); Set up and read the blob into an array to be sent fr = FileReader(); fr.onload = () { oldSend.call(oXHR,fr.result); }; fr.onerror = (err) { throw err; }; fr.readAsArrayBuffer(data); Set the multipart content type and boudary this.setRequestHeader('Content-Type','multipart/form-data; boundary=' + boundary); XMLHttpRequest.prototype.send = oldSend; } { oldSend.call(View Code
贴出后台的实现(nodejs):
*移动端图片压缩上传功能后台 */ "use strict"; var fs = require('fs'var router = require("../router"var FormParser = require("./formParser"); var formidable = require('formidable'var path = require('path'); var fileSaveDir = path.join(STATIC_PATH,'upload'); router.setMap({ "uindex_2": path.join(__dirname,"./index_2.html": cupload }); cupload(req,res) { fs.existsSync(fileSaveDir)) { fs.mkdirSync(fileSaveDir) } var form = formidable.IncomingForm(); var responseData = []; form.uploadDir = fileSaveDir; form.type = true; form.keepExtensions = ; form.parse(req,1)">(err,fields,files){ if(!err) { Object.keys(files).forEach((key){ var file = files[key]; var filename = path.basename(file.path); 每张图片给予一分钟保存时间 setTimeout(() { if (!fs.existsSync(file.path)) ; console.log("\x1B[33m删除文件" + filename + "\x1B[0m"); fs.unlinkSync(file.path); },60 * 1000 塞入响应数据中 responseData.push({ type: file.type,name: filename,path: '/public/upload/' + filename,size: file.size / 1024 > 1024 ? (~~(10 * file.size / 1024 / 1024)) / 10 + "MB" : ~~(file.size / 1024) + "KB" }); }); } { console.warn(err); } res.writeHead(200); res.end(JSON.stringify(responseData)); }); }
转载地址:http://www.cnblogs.com/axes/p/4603984.html
相关链接:http://stackoverflow.com/questions/15639070/empty-files-uploaded-in-android-native-browser/28809955#28809955
深入研究HTML5实现图片压缩上传:http://www.cnblogs.com/hutuzhu/p/5265023.html
前端本地客户端压缩图片,兼容IOS,Android,PC、自动按需加载文件:https://github.com/think2011/localResizeIMG
原文链接:/html5/994364.html