前言
今天我们要讲解下如何编写一个图片压缩方向纠正插件, 附带着会讲解下如何上传和预览
为什么重点放在图片压缩和方向纠正?
相信大家在做项目过程中, 经常会遇到上传图片到后端, 但是由于图片过大, 需要对图片压缩处理特别在移动端, 手机拍的照片普遍过于大了, 我们有时候只是需要上传一张头像, 很小就够用了还有在部分手机上 (已知苹果手机) 拍的照片存在方向角度问题, 这时就需要我们来纠正图片角度了
很多同学多数时候是在用别人写好的图片压缩上传插件针对我们的需求, 这些插件有时候不能达到我们最理想的效果, 自己写呢, 又不会写, 很是头疼今天就深入剖析讲解下, 教会大家编写自己的图片压缩方向纠正插件, 以及预览和上传压缩后图片数据
文中用到的一些 H5 的 api 和 EXIF.js 等知识点如果不懂的话, 请先阅读文末尾的结语中的知识点资料
实现原理
压缩图片并且上传主要用到 filereadercanvas 以及 formdata 这三个 h5 的 api 和插件 EXIF.js 逻辑并不难整个过程就是:
(1)用户使用 input file 上传图片的时候, 用 filereader 读取用户上传的图片数据(base64 格式)
(2)把图片数据传入 img 对象, 然后将 img 绘制到 canvas 上, 用 EXIF.js 对图片方向进行纠正, 再调用 canvas.toDataURL 对图片进行压缩, 获取到压缩后的 base64 格式图片数据, 转成二进制
(3)获取到压缩后的图片二进制数据, 预览 (4)将压缩后的图片二进制数据塞入 formdata, 再通过 XmlHttpRequest 提交 formdata
如此四步, 就完成了图片的压缩方向纠正预览和上传
插件设计思考
考虑到在实际项目中, 可能用不同的开发框架(vue.js/JQ/react.js/angular.js/anu.js 等), 图片预览的 UI 样式也可能不同, 图片数据上传方法可能不同因为图片压缩和方向纠正这两块的逻辑多变性比较低, 我们这里把图片压缩和方向纠正抽离出来, 封装为一个插件库
一获取图片数据
先是获取图片数据, 也就是监听 input file 的 change 事件, 然后获取到用来压缩上传的文件对象 files, 将 files 传到图片压缩方向纠正插件中进行处理
这时候根据每个人的需求, 也可以预览未压缩的图片
- // 监听上传组件 input 的 onchange 事件, 压缩图片, 纠正图片方向, 同时获取压缩后的图片
- filechooser.onchange = function () {
- var fileList = this.files;
- // 预览压缩前的图片
- var files = Array.prototype.slice.call(fileList);
- files.forEach(function (file, i) {
- var reader = new FileReader();
- reader.onload = function () {
- var li = document.createElement("li")
- li.style.backgroundImage = 'url('+this.result+')';
- document.querySelector('.img_list').appendChild(li)
- }
- reader.readAsDataURL(file);
- });
- // 处理图片列表, getCompressiveFileList 接受处理后的图片数据列表
- // 下面两行代码为图片压缩方向纠正插件的用法, 具体实现细节请继续往下阅读 ~_~
- var process = window.lzImgProcess();
- process(fileList, getCompressiveFileList);
- }
二图片压缩方向纠正插件实现
上面做完图片数据的获取后, 就可以做 process 压缩图片的方法了而压缩图片也并不是直接把图片绘制到 canvas 再调用一下 toDataURL 就行的
在 IOS 中, canvas 绘制图片是有两个限制的:
首先是图片的大小, 如果图片的大小超过两百万像素, 图片也是无法绘制到 canvas 上的, 调用 drawImage 的时候不会报错, 但是你用 toDataURL 获取图片数据的时候获取到的是空的图片数据
再者就是 canvas 的大小有限制, 如果 canvas 的大小大于大概五百万像素 (即宽高乘积) 的时候, 不仅图片画不出来, 其他什么东西也都是画不出来的
应对上面两种限制, 我把图片宽度高度压缩控制在 1000px 以内, 这样图片最大就不超过两百万像素了在前端开发中, 1000px*1000px 基本可以满足绝大部分的需求了当然了还有更完美的瓦片式绘制的方法, 我们这里就说瓦片式绘制方法了
如此一来就解决了 IOS 上的两种限制了
除了上面所述的限制, 还有两个坑, 一个就是 canvas 的 toDataURL 是只能压缩 jpg 的, 当用户上传的图片是 png 的话, 就需要转成 jpg, 也就是统一用 canvas.toDataURL('image/jpeg', 0.5) , 类型统一设成 jpeg, 而压缩比就自己控制了
另一个就是如果是 png 转 jpg, 绘制到 canvas 上的时候, canvas 存在透明区域的话, 当转成 jpg 的时候透明区域会变成黑色, 因为 canvas 的透明像素默认为 rgba(0,0,0,0), 所以转成 jpg 就变成 rgba(0,0,0,1)了, 也就是透明背景会变成了黑色解决办法就是绘制之前在 canvas 上铺一层白色的底色
在压缩图片之前, 我们判断图片角度, 如果图片角度不正确, 还需要用 EXIF.js 把图片角度纠正过来
压缩完图片, 把 base64 的图片数据转成二进制数据存储到暂存区中, 等待被 getBlobList 获取使用
- (function(window) {
- /**
- *
- * 作者: 混沌传奇
- *
- * 邮箱地址: iot-pro_lizeng@foxmail.com
- *
- * 日期: 2017-10-26
- *
- * 插件功能: 压缩图片 && 纠正图片方向 && 返回二进制 (Blob) 图片元数据组成的列表
- *
- */
- window.lzImgProcess = function() {
- var Orientation = '',
- // 图片方向角
- blobList = [],
- // 压缩后的二进制图片数据列表
- canvas = document.createElement("canvas"); // 用于压缩图片 (纠正图片方向) 的 canvas
- ctx = canvas.getContext('2d'),
- file_type = 'image/jpeg',
- // 图片类型
- qlty = 0.5,
- // 图片压缩品质, 默认是 0.5, 可选范围是 0-1 的数字类型的值, 可配置
- imgWH = 1000; // 压缩后的图片的最大宽度和高度, 默认是 1000px, 可配置
- /**
- * @actionName process,
- * 方法功能: 压缩图片 && 纠正图片方向 && 返回二进制 (Blob) 图片元数据
- *
- * @param fileList, 传入函数的文件列表对象, fileList 对象是来自用户在一个 < input > 元素上选择文件后返回的 FileList 对象
- * 注意: 图片类型必须是 jpeg||png
- * 比如:<input id="uploadImage" onchange="loadImageFile();" />
- * function loadImageFile() {
- * // 获取返回的 fileList 对象
- * var fileList = document.getElementById("uploadImage").files;
- * }
- * @param getBlobList [Blob], 获取压缩结果的钩子函数, 接受一个参数
- * 功能: 在图片压缩完毕后, 获取压缩后的二进制图片数据对象组成的数组, 参数即: 压缩后的二进制图片数据 (blob) 组成的 list
- *
- * @param quality, 传入函数的图片压缩比率(品质), 可选范围 0-1 的数字类型的值, 默认是 0.5
- *
- * @param WH, 传入函数的图片压缩后的最大图片宽度和高度, 默认是 1000, 单位是 px, 可自由配置
- * 注意: 最好不要超过 1000, 数字过大, 容易导致 canvas 压缩失败由于没做瓦片处理, 所以有这个限制 1000*1000 的图片在前端中, 基本也够用了
- *
- */
- function process(fileList, getBlobList, quality, WH) {
- blobList = []; // 初始化 blobList
- // 判断参数 fileList 的长度是否大于 0
- if (!fileList.length) {
- console.log('警告: 传进方法 process 的参数 fileList 长度必须大于零!!!') return;
- }
- // 如果 quality 参数有值, 则把 quality 赋值给 qlty(图片压缩的品质)
- if (quality) qlty = quality;
- // 如果 WH 参数有值, 则把 WH 赋值给 imgWH(压缩后的图片的最大宽度和高度)
- if (WH && WH < 1000 && WH > 0) {
- imgWH = WH;
- }
- // 把传进来的 fileList 转为数组类型
- var files = Array.prototype.slice.call(fileList);
- files.forEach(function(file, i) {
- if (!/\/(?:jpeg|png)/i.test(file.type)) {
- console.log('警告: 图片必须是 jpeg||png 类型!!!');
- return;
- }
- // file_type = file.type;
- var reader = new FileReader();
- // 获取图片压缩前大小, 打印图片压缩前大小
- var size = file.size / 1024 > 1024 ? (~~ (10 * file.size / 1024 / 1024)) / 10 + "MB": ~~ (file.size / 1024) + "KB";
- // console.log('size:', size)
- reader.onload = function() {
- var img = new Image();
- img.src = this.result;
- // 图片加载完毕之后进行压缩
- if (img.complete) {
- callback();
- } else {
- img.onload = callback;
- }
- function callback() {
- // 获取照片方向角属性, 用户旋转控制
- EXIF.getData(img,
- function() {
- // alert(EXIF.pretty(this));
- EXIF.getAllTags(this);
- // alert(EXIF.getTag(this, 'Orientation'));
- Orientation = EXIF.getTag(this, 'Orientation');
- if (Orientation == "" || Orientation == undefined || Orientation == null) {
- Orientation = 1;
- }
- });
- // 获取压缩后的图片二进制数据
- var data = GetImgCompress(img);
- // 将二进制数据塞入到二进制数据列表中
- blobList.push(data);
- // 将压缩后的二进制图片数据对象 (blob) 组成的 list 通过钩子函数返回出去
- if (blobList.length === files.length) {
- if (getBlobList) getBlobList(blobList);
- }
- img = null;
- }
- };
- reader.readAsDataURL(file);
- })
- }
- /**
- * @actionName GetImgCompress,
- * 功能: 判断上传图片的方向, 如果不是正确的, 进行修正, 并对图片进行压缩, 压缩完后, 返回压缩后的二进制图片数据
- *
- * @param img, 用来压缩的图片对象
- *
- * @returns 返回的压缩后的二进制图片数据
- */
- function GetImgCompress(img) {
- if (navigator.userAgent.match(/iphone/i)) {
- //console.log('iphone');
- // 如果方向角不为 1, 都需要进行旋转
- if (Orientation != "" && Orientation != 1) {
- switch (Orientation) {
- case 6:
- // 需要顺时针(向左)90 度旋转
- rotateImg(img, 'left', canvas);
- break;
- case 8:
- // 需要逆时针(向右)90 度旋转
- rotateImg(img, 'right', canvas);
- break;
- case 3:
- // 需要 180 度旋转
- rotateImg(img, 'right', canvas); // 转两次
- rotateImg(img, 'right', canvas);
- break;
- }
- } else {
- // 不做旋转
- rotateImg(img, 'no', canvas);
- }
- } else if (navigator.userAgent.match(/Android/i)) { // 修复 android
- if (Orientation != "" && Orientation != 1) {
- switch (Orientation) {
- case 6:
- // 需要顺时针(向左)90 度旋转
- rotateImg(img, 'left', canvas);
- break;
- case 8:
- // 需要逆时针(向右)90 度旋转
- rotateImg(img, 'right', canvas);
- break;
- case 3:
- // 需要 180 度旋转
- rotateImg(img, 'right', canvas); // 转两次
- rotateImg(img, 'right', canvas);
- break;
- }
- } else {
- // 不做旋转
- rotateImg(img, 'no', canvas);
- }
- } else {
- if (Orientation != "" && Orientation != 1) {
- switch (Orientation) {
- case 6:
- // 需要顺时针(向左)90 度旋转
- rotateImg(img, 'left', canvas);
- break;
- case 8:
- // 需要逆时针(向右)90 度旋转
- rotateImg(img, 'right', canvas);
- break;
- case 3:
- // 需要 180 度旋转
- rotateImg(img, 'right', canvas); // 转两次
- rotateImg(img, 'right', canvas);
- break;
- }
- } else {
- // 不做旋转
- rotateImg(img, 'no', canvas);
- }
- }
- var ndata;
- ndata = canvas.toDataURL(file_type, qlty);
- // 打印压缩前后的大小, 以及压缩比率
- // var initSize = img.src.length;
- // console.log('压缩前:' + initSize);
- // console.log('压缩后:' + ndata.length, 'base64 数据', ndata);
- // console.log('压缩率:' + ~~(100 * (initSize - ndata.length) / initSize) + "%");
- // 将压缩后的 base64 数据转为二进制数据
- ndata = dataURItoBlob(ndata);
- // 清除 canvas 画布的宽高
- canvas.width = canvas.height = 0;
- return ndata;
- }
- /**
- * @actionName rotateImg,
- * 功能: 对图片旋转处理
- *
- * @param img, 用来矫正方向的图片对象
- *
- * @param direction, 旋转方向
- *
- * @param canvas, 用来绘制图片的 cavas 画布对象
- */
- function rotateImg(img, direction, canvas) {
- // 最小与最大旋转方向, 图片旋转 4 次后回到原方向
- var min_step = 0;
- var max_step = 3;
- if (img == null) return;
- //img 的高度和宽度不能在 img 元素隐藏后获取, 否则会出错
- var height = img.height;
- var width = img.width;
- if (width > imgWH || height > imgWH) {
- var ratio = ~~ (height / width * 10) / 10;
- if (width > height) {
- width = imgWH;
- height = imgWH * ratio;
- } else {
- height = imgWH;
- width = height / ratio;
- }
- img.width = width;
- img.height = height;
- }
- canvas.width = width;
- canvas.height = height;
- // 铺底色
- ctx.fillStyle = "#fff";
- ctx.fillRect(0, 0, width, height);
- var step = 2;
- if (step == null) {
- step = min_step;
- }
- if (direction == 'no') {
- step = 0;
- } else if (direction == 'right') {
- step++;
- // 旋转到原位置, 即超过最大值
- step > max_step && (step = min_step);
- } else {
- step--;
- step < min_step && (step = max_step);
- }
- // 旋转角度以弧度值为参数
- var degree = step * 90 * Math.PI / 180;
- switch (step) {
- case 0:
- ctx.drawImage(img, 0, 0, width, height);
- break;
- case 1:
- ctx.rotate(degree);
- ctx.drawImage(img, 0, -height, width, height);
- break;
- case 2:
- ctx.rotate(degree);
- ctx.drawImage(img, -width, -height, width, height);
- break;
- case 3:
- ctx.rotate(degree);
- ctx.drawImage(img, -width, 0, width, height);
- break;
- }
- }
- /**
- * dataURL to blob, ref to https://gist.github.com/fupslot/5015897
- * @param dataURI, 图片的 base64 格式数据
- * @returns {Blob}
- */
- function dataURItoBlob(dataURI) {
- var byteString = atob(dataURI.split(',')[1]);
- var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
- var ab = new ArrayBuffer(byteString.length);
- var ia = new Uint8Array(ab);
- for (var i = 0; i < byteString.length; i++) {
- ia[i] = byteString.charCodeAt(i);
- }
- return new Blob([ab], {
- type: mimeString
- });
- }
- /**
- * 返回一个 process 方法
- *
- * process 方法: 用来压缩图片数据, 在压缩图片的同时, 默认会调用 correctOrientation 方法纠正图片方向
- *
- */
- return process;
- }
- })(window)
Exif.js 提供了 JavaScript 读取图像的原始数据的功能扩展, 例如: 拍照方向相机设备型号拍摄时间 ISO 感光度 GPS 地理位置等数据
Exif.js 官方 github 仓库地址: github.com/exif-js/exi
三获取压缩后的图片二进制数据, 预览图片
完成图片压缩后, 就可以获取压缩后的图片二进制数据了, 把获取到的图片二进制数据存起来; 获取到数据后, 可以拿来预览
由于实际项目中, 每个项目的 UI 样式设计可能不一样, 开发者可以根据自己的 UI 样式来预览图片
- // 获取压缩后的图片
- function getCompressiveFileList(fileList) {
- blobFileList = fileList;
- // console.log('fileBlobList:', fileList);
- fileList.forEach(function (blob) {
- var reader = new FileReader();
- reader.onload = function () {
- var li = document.createElement("LI")
- li.style.backgroundImage = 'url('+this.result+')';
- document.querySelector('.imgCompress_list').appendChild(li)
- }
- reader.readAsDataURL(blob);
- })
- }
四提交图片数据到后台
new 一个 formdata 对象, 将上一步获取到的 blobFileList 图片二进制数据 append 到 formdata 中, 用任意你喜欢的 ajax 库进行上传当然也可以用原生 ajax 上传
- // 将压缩后的二进制图片数据流 append 到 formdata 对象中上传到后台服务器
- // 注意: 上传的是 formdata 对象, 后台接口接收的时候, 也要从 formdata 对象中读取二进制数据流
- function formUpData(blobFiles){
- var formData = new FormData();
- formData.append("files", blobFiles);
- var xhr = new XMLHttpRequest();
- // 链接你自己上传图片接口即可, 这里的接口地址, 是我写的示例, 不可真实使用, 讲解意义更大
- xhr.open('post', 'http://xxx/welcome/index/');
- xhr.onreadystatechange = function () {
- if (xhr.readyState == 4 && xhr.status == 200) {
- console.log('上传成功!');
- }
- };
- xhr.send(formData);
- }
结语
完整代码以及 demo:git 仓库地址 https://github.com/legend-li/img_compress_rotate_preview_upload
来源: https://juejin.im/post/5a9759a16fb9a0635b5360b3