很久没有更新博客了,再不写点东西都烂了。
这次更新一个小内容,是两个插件的组合使用,实现头像上传功能。
成果预览:
Jcrop:用于前端“裁剪”图片
bootstrap-fileinput:用于前端优化上传控件样式ARTtemplate:JS版的JSTL?反正就是一个腾讯的模板化插件,很好用,真心。bootstrap-sco.modal.js:这个是bootstrap的一个模态插件
SpringMVC:使用框架自带的MultipartFile来获取文件(效率能够大大的提高)
Image:这个是Java的内置类,用于处理图片很方便。首先是Jcrop这个前端JS插件,这个插件很好用,其实在各大网站中也很常见,先上图:
没错,这个一脸懵逼的就是我。。。
说说原理,实际上,Jcrop并没有在客户端帮我们把图片进行裁剪,只是收集了用户的“裁剪信息”,然后传到后端,最后的裁剪和压缩,还是要依靠服务器上的代码来进行。
我们可以看到这个插件在图片上显示出了8个控制点,让用户选择裁剪区域,当用户选择成功后,会自动的返回“裁剪信息”,所谓的裁剪信息,其实就是选框的左上角原点坐标,及裁剪框的宽度和高度,通过这四个值,在后端就可以进行裁剪工作了。
但是我们要注意,用户在上传图片的时候,长度宽度都是不规则的,当然我们可以用bootstap-fileinput这个插件去限制用户只能上传指定宽高的图片,但这就失去了我们“裁剪”的意义,而且用户的体验就非常差劲。然而jcrop所返回的坐标值及宽高,并不是基于所上传图片自身的像素,而是如图中所示,是外层DIV的宽高。举一个例子,上图我实际放入的个人照片宽度是852px,但是Jcrop的截取宽度是312px,这个312px并不是真正图片上的实际宽度,是经过缩放后的宽度,所以我们后端一定需要重新对这个312px进行一次还原,还原到照片实际比例的宽度。
好啦,原理就是这样子。接下来,就是上代码了。
1 <script id="portraitUpload" type="text/html"> 2 <div style="padding: 10px 20px"> 3 <form role="form" enctype="multipart/form-data" method="post"> 4 <div class="embed-responsive embed-responsive-16by9"> 5 <div class="embed-responsive-item pre-scrollable"> 6 <img alt="" src="${pageContext.request.contextPath}/img/showings.jpg" id="cut-img" 7 class="img-responsive img-thumbnail"/> 8 </div> 9 </div> 10 <div class="white-divider-md"></div> 11 <input type="file" name="imgFile" id="fileUpload"/> 12 <div class="white-divider-md"></div> 13 <div id="alert" class="alert alert-danger hidden" role="alert"></div> 14 <input type="hidden" id="x" name="x"/> 15 <input type="hidden" id="y" name="y"/> 16 <input type="hidden" id="w" name="w"/> 17 <input type="hidden" id="h" name="h"/> 18 </form> 19 </div> 20 </script>
这个就是一个ArtTemplate的模板代码,就写在</body>标签上方就行了,因为text/html这个类型,不会被识别,所以实际上用Chrome调试就可以看得到,前端用户是看不到这段代码的。
简单解释一下这个模板,这个模板是我最后放入模态窗口时用的模板,就是把这段代码,直接丢进模态弹出来的内容部分。因为是文件上传,自然需要套一个<form>标签,然后必须给form标签放入 enctype="multipart/form-data",否则后端Spring就无法获取这个文件。
"embed-responsive embed-responsive-16by9"这个类就是用来限制待编辑图片加载后的宽度大小,值得注意的是,我在其内种,加了一个
<div class="embed-responsive-item pre-scrollable">
pre-scrollable这个类,会让加载的图片不会因为太大而“变形”,因为我外层通过embed-responsive-16by9限制死了图片的宽高,图片本身又加了img-responsive这个添加响应式属性的类,为了防止图片缩放,导致截图障碍,所以就给内层加上pre-scrollable,这个会给图片这一层div加上滚动条,如果图片高度太高,超过了外层框,则会出现滚动条,而这不会影响图片截取,同时又保证了模态窗口不会“太长”,导致体验糟糕(尤其在移动端)。
底下四个隐藏域相信大家看他们的name值也就知道个大概,这个就是用于存放Jcrop截取时所产生的原点坐标和截取宽高的值。
这个JS是上传页面的相关逻辑。会JS的人都看得懂它的意义,我就简单说一下几个事件的意义:
1 portrait.on('fileuploaderror', function (event, data, msg) { 2 alert.removeClass('hidden').html(msg); 3 fileUp.fileinput('disable'); 4 });这个事件,是用于bootstrap-fileinput插件在校验文件格式、文件大小等的时候,如果不符合我们的要求,则会对前面HTML代码中有一个
<div id="alert" class="alert alert-danger hidden" role="alert"></div>
进行一些错误信息的显示操作。
1 portrait.on('fileclear', function (event) { 2 alert.addClass('hidden').html(); 3 });这部分代码,是当文件移除时,隐藏错误信息提示区,以及清空内容,当然这是符合我们的业务逻辑的。
1 portrait.on('fileloaded', function (event, file, previewId, index, reader) { 2 alert.addClass('hidden').html(); 3 });这部分代码是当选择文件时(此时还没进行文件校验),隐藏错误信息,清空错误内容,这么做是为了应对如果上一次文件校验时有错误,而重新选择文件时,肯定要清空上一次的错误信息,再显示本次的错误信息。
1 portrait.on('fileuploaded', function (event, data) { 2 if (!data.response.status) { 3 alert.html(data.response.message).removeClass('hidden'); 4 } 5 })这部分是当文件上传后,后端如果返回了错误信息,则需要进行相关的提示信息处理。
1 this.getExtraData = function () { 2 return { 3 sw: $('.jcrop-holder').css('width'), 4 sh: $('.jcrop-holder').css('height'), 5 x: $('#x').val(), 6 y: $('#y').val(), 7 w: $('#w').val(), 8 h: $('#h').val() 9 } 10 }这部分代码是获取上传文件时,附带需要发往后端的参数,这里面可以看到,x、y自然是Jcrop截取时,选框的左上角原点坐标,w、h自然就是截取的宽高,但是刚才我说了,这个是经过缩放后的宽高,不是依据图片实际像素的宽高。而sw、sh代表的是scaleWidth、scaleHeight,就是缩放宽高的意思。这个.jcrop-holder的对象是当Jcrop插件启用后,加载的图片外层容器的对象,只需要获取这个对象的宽高,就是图片被压缩的宽高,但是因为我限制了图片的宽度和高度,宽度的比例是定死的(不是宽高定死,只是比例定死,bootstrap本身就是响应式框架,所以不能单纯的说宽高定死,宽高会随着使用终端的变化而变化),高度是根据宽度保持16:4,可是我又加了pre-scrollable这个类让图片过高时以滚动条的方式不破坏外层容器的高度,所以我们实际能拿来计算缩放比例的,是宽度,而不是高度,但是这里我一起传,万一以后有其他的使用场景,要以高度为准也说不定。
好了,然后我需要贴上bootstrap-fileinput插件的配置代码:
1 this.portrait = function (target, uploadUrl, data, alert) { 2 target.fileinput({ 3 language: 'zh', //设置语言 4 maxFileSize: 2048,//文件最大容量 5 uploadExtraData: data,//上传时除了文件以外的其他额外数据 6 showPreview: false,//隐藏预览 7 uploadAsync: true,//ajax同步 8 dropZoneEnabled: false,//是否显示拖拽区域 9 uploadUrl: uploadUrl, //上传的地址 10 elErrorContainer: alert,//错误信息内容容器 11 allowedFileExtensions: ['jpg'],//接收的文件后缀 12 showUpload: true, //是否显示上传按钮 13 showCaption: true,//是否显示标题 14 browseClass: "btn btn-primary", //按钮样式 15 previewFileIcon: "<i class='glyphicon glyphicon-king'></i>", 16 ajaxSettings: {//这个是因为我使用了SpringSecurity框架,有csrf跨域提交防御,所需需要设置这个值 17 beforeSend: function (xhr) { 18 xhr.setRequestHeader(header, token); 19 } 20 } 21 }); 22 this.alert(target, alert); 23 }; 24 this.alert = function (target, alert) { 25 target.on('fileuploaderror', function (event, data, msg) { 26 alert.removeClass('hidden').html(msg); 27 _this.fileinput('disable'); 28 }); 29 target.on('fileclear', function (event) { 30 alert.addClass('hidden').html(); 31 }); 32 target.on('fileloaded', function (event, file, previewId, index, reader) { 33 alert.addClass('hidden').html(); 34 }); 35 target.on('fileuploaded', function (event, data) { 36 if (!data.response.status) { 37 alert.html(data.response.message).removeClass('hidden'); 38 } 39 }); 40 };这个代码有写了注释,我就不多解释了。
唯一做一个补充,就是“elErrorContainer: alert,//错误信息内容容器”这个配置,这个是我后来再次研究这个插件得到的一个心得,这个插件自带错误信息显示的功能,但是吧,至少我还不知道如何能够让ajax后自定义的错误信息调用这个显示功能,于是我就只能自己定义一个alert的容器,用来存放错误信息来扩展这个插件,但是这样就会在某种情况下比如400错误时,导致出现两个错误信息提示,那解决的办法我是看到了这个参数,只需要将这个错误信息的容器从默认值修改为我自定义的容器就可以了。
关于Ajax同步,是因为我个人认为,上传文件这个还是做成同步比较好,等文件上传完成后,js代码才能继续执行下去。因为文件上传毕竟是一个耗时的工作,有的逻辑又确实需要当文件上传成功以后才执行,比如刷新页面,所以为了避免出现问题,还是做成同步的比较好。还有就是去掉预览,用过bootstrap-fileinput插件的都知道,这个插件的图片预览功能很强大,甚至可以单独依靠这个插件来制作相册管理。但是因为我们这次要结合Jcrop,所以要割掉这部分功能。
缩放比例scale一定要用double,并且宽高也要转换成double后再相除,否则会变成求模运算,这样会降低精度,别小看这里的精度下降,最终的截图效果根据图片的缩放程度,误差可是有可能被放大的很离谱的。
我专门把图片压缩写成了一个类。
其中可以看到一些关于文件路径的方法,其实没有什么特别的,就是截取后缀获取路径之类的,我这边也贴出来吧,免得有些朋友看的云里雾里的。
1 package com.magic.rent.tools; 2 3 import com.magic.rent.exception.custom.BusinessException; 4 5 import javax.imageio.ImageIO; 6 import java.awt.*; 7 import java.awt.image.BufferedImage; 8 import java.awt.image.CropImageFilter; 9 import java.awt.image.FilteredImageSource; 10 import java.awt.image.ImageFilter; 11 import java.io.File; 12 import java.util.ArrayList; 13 14 /** 15 * 知识产权声明:本文件自创建起,其内容的知识产权即归属于原作者,任何他人不可擅自复制或模仿. 16 * 创建者: wu 创建时间: 2016/11/25 17 * 类说明: 18 * 更新记录: 19 */ 20 public class FileTools { 21 22 public static final int IMG = 1; 23 24 /** 25 * 获取项目根目录 26 * 27 * @return 根目录 28 */ 29 public static String getWebRootPath() { 30 return System.getProperty("web.root"); 31 } 32 33 /** 34 * 获取头像目录,若不存在则直接创建一个 35 * 36 * @param userID 用户ID 37 * @return 38 */ 39 public static String getPortraitPath(int userID) { 40 String realPath = getWebRootPath() + "img/portrait/" + userID + "/"; 41 File file = new File(realPath); 42 //判断文件夹是否存在,不存在则创建一个 43 if (!file.exists() || !file.isDirectory()) { 44 if (!file.mkdirs()) { 45 throw new BusinessException("创建头像文件夹失败!"); 46 } 47 } 48 return realPath; 49 } 50 51 /** 52 * 重命名头像文件 53 * 54 * @param fileName 文件名 55 * @return 56 */ 57 public static String getPortraitFileName(String fileName) { 58 // 获取文件后缀 59 String suffix = getSuffix(fileName); 60 return "portrait" + suffix; 61 } 62 63 /** 64 * 判断文件后缀是否符合要求 65 * 66 * @param fileName 文件名 67 * @param allowSuffix 允许的后缀集合 68 * @return 69 * @throws Exception 70 */ 71 public static boolean checkSuffix(String fileName, String[] allowSuffix) throws Exception { 72 String fileExtension = getSuffix(fileName); 73 boolean flag = false; 74 for (String extension : allowSuffix) { 75 if (fileExtension.equals(extension)) { 76 flag = true; 77 } 78 } 79 return flag; 80 } 81 82 83 public static String getSuffix(String fileName) { 84 return fileName.substring(fileName.lastIndexOf(".")).toLowerCase(); 85 } 86 87 /** 88 * 将文件地址转成链接地址 89 * 90 * @param filePath 文件路径 91 * @param fileType 文件类型 92 * @return 93 */ 94 public static String filePathToSRC(String filePath, int fileType) { 95 String href = ""; 96 if (null != filePath && !filePath.equals("")) { 97 switch (fileType) { 98 case IMG: 99 if (filePath.contains("/img/")) { 100 int index = filePath.indexOf("/img/"); 101 href = filePath.substring(index); 102 } else { 103 href = ""; 104 } 105 return href; 106 } 107 } 108 return href; 109 } 110 111 /** 112 * 获取指定文件或文件路径下的所有文件清单 113 * 114 * @param fileOrPath 文件或文件路径 115 * @return 116 */ 117 public static ArrayList<File> getListFiles(Object fileOrPath) { 118 File directory; 119 if (fileOrPath instanceof File) { 120 directory = (File) fileOrPath; 121 } else { 122 directory = new File(fileOrPath.toString()); 123 } 124 125 ArrayList<File> files = new ArrayList<File>(); 126 127 if (directory.isFile()) { 128 files.add(directory); 129 return files; 130 } else if (directory.isDirectory()) { 131 File[] fileArr = directory.listFiles(); 132 if (null != fileArr && fileArr.length != 0) { 133 for (File fileOne : fileArr) { 134 files.addAll(getListFiles(fileOne)); 135 } 136 } 137 } 138 139 return files; 140 } 141 142 143 /** 144 * 截图工具,根据截取的比例进行缩放裁剪 145 * 146 * @param path 图片路径 147 * @param zoomX 缩放后的X坐标 148 * @param zoomY 缩放后的Y坐标 149 * @param zoomW 缩放后的截取宽度 150 * @param zoomH 缩放后的截取高度 151 * @param scaleWidth 缩放后图片的宽度 152 * @param scaleHeight 缩放后的图片高度 153 * @return 是否成功 154 * @throws Exception 任何异常均抛出 155 */ 156 public static boolean imgCut(String path, int zoomX, int zoomY, int zoomW, 157 int zoomH, int scaleWidth, int scaleHeight) throws Exception { 158 Image img; 159 ImageFilter cropFilter; 160 BufferedImage bi = ImageIO.read(new File(path)); 161 int fileWidth = bi.getWidth(); 162 int fileHeight = bi.getHeight(); 163 double scale = (double) fileWidth / (double) scaleWidth; 164 165 double realX = zoomX * scale; 166 double realY = zoomY * scale; 167 double realW = zoomW * scale; 168 double realH = zoomH * scale; 169 170 if (fileWidth >= realW && fileHeight >= realH) { 171 Image image = bi.getScaledInstance(fileWidth, fileHeight, Image.SCALE_DEFAULT); 172 cropFilter = new CropImageFilter((int) realX, (int) realY, (int) realW, (int) realH); 173 img = Toolkit.getDefaultToolkit().createImage( 174 new FilteredImageSource(image.getSource(), cropFilter)); 175 BufferedImage bufferedImage = new BufferedImage((int) realW, (int) realH, BufferedImage.TYPE_INT_RGB); 176 Graphics g = bufferedImage.getGraphics(); 177 g.drawImage(img, 0, 0, null); 178 g.dispose(); 179 //输出文件 180 return ImageIO.write(bufferedImage, "JPEG", new File(path)); 181 } else { 182 return true; 183 } 184 } 185 }顺便一提:getWebRootPath这个方法,要生效,必须在Web.xml中做一个配置:
1 <context-param> 2 <param-name>webAppRootKey</param-name> 3 <param-value>web.root</param-value> 4 </context-param>否则是无法动态获取项目的本地路径的。这个配置只要跟在Spring配置后面就行了,应该就不会有什么大碍,其实就是获取本地路径然后设置到系统参数当中。
好了,这就是整个插件的功能了。
https://github.com/wuxinzhe/Portrait.git 如果有帮助,希望给个Star
转载于:https://www.cnblogs.com/wuxinzhe/p/6198506.html
相关资源:数据结构—成绩单生成器