SpringMVC+JQuery实现头像编辑器
一、简单说明
本头像编辑器主要实现了图片的上传、显示(不溢出父窗口)、旋转、裁剪功能!
- 图片的上传用到的是异步上传,页面不进行刷新,原理是通过JQuery的异步提交+SpringMVC的上传
- 上传完毕后,在显示的时候,如果图片的长宽都比父窗口小,则垂直 水平 居中显示在父窗口中,否则,对图片按照宽高比进行缩放,直到长宽都不溢出!
- 旋转功能,这块有待改进!我在上传一张图片的时候,直接在后台将90度,180度,270度的也上传了,然后,裁剪完成后,保存头像之后,将不用的都删除!这儿确实不太现实!这里推荐用HTML5的canvas实现!
- 裁剪功能。用到了JQuery的一个插件。参见区域选择完成后,会自动返回 x,y,w,h。这里的x,y,w,h还需要经过处理,因为你前面可能进行了缩小显示,这里需要让x,y,w,h还原到未缩放前的坐标。然后,在后台中,通过这些坐标,截取图片中 (x,y) 宽:w 高:h的图片!
二、效果图:
- 初始化界面
2.选择图片
3.显示选择的图片
4.旋转图片
5.拖动鼠标选择裁剪框
6.保存头像,并且显示
三、代码实现
1.图片的选择以及上传
我们肯定都见过自带的上传按钮,这个按钮比较难看。,这个是不是比上一张强多了!其实就是 一个buton按钮,然后将上面的<input type="file">隐藏了,然后当点击button按钮的时候,调用file的事件。这样就实现了和点击file一样的效果。
<input type="button" value="选择头像" class="bt"><font color="#999999"><span> 支持jpg、jpeg、gif、png、bmp格式的图片</span> <input type="file" id="file" name="file"> <!--可以在css中设置隐藏属性或者在js中设置-->
1 $(".bt").click(function(){ 2 $("#file").click(); 3 });
这样,就实现了弹出选择框的功能。接下来需要实现的就是选择图片。这里需要注意的是,现在由于浏览器的安全机制,默认情况下,你是不能直接访问本地图片地址,例如<img src="d:1.png">这种情况下在jsp页面中完全不能用,除非你对浏览器进行设置。但是这么是不合理的。所以,最好能够上传图片到服务器。
SpringMVC中,上传图片用到了"commons-fileupload-1.2.2.jar"这个插件,如果要使用图片上传,需要注意一下几点:
- 在SpringMVC的配置文件中,添加一下内容
1 <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> 2 <property name="defaultEncoding" value="utf-8" /> 3 <property name="maxUploadSize" value="10485760000" /> 4 <property name="maxInMemorySize" value="40960" /> 5</bean>
- 在HTML中,表单的提交中,需要设置'enctype="multipart/form-data" '
上传完图片后,这里不像再去跳转,所以,这里我用到了异步上传,所谓的异步上传,其实就是异步提交表单。异步提交用得到了JQuery的异步提交插件:"jquery-form.js";
1 $("#file").change(function(){ 2 3 var options = { 4 url : "upload/image", //这个url是异步提交的路径,这里对应的是上传图片的路径 5 dataType : 'json', 6 contentType : "application/json; charset=utf-8", 7 success : function(data) { 8 //上传成功后,需要进行哪些处理,比方说这里实现的是显示图片 9 10 }; 11 $("#uploadImgForm").ajaxSubmit(options); 12 });
如上所示,就实现了图片的异步上传,那么后台是如何处理?这里 是先上传原图,然后再用一个旋转类,将该图片90度,180度,270度的图片保存成功。以供旋转使用,这里是一个大问题,其实我们可以使用html5的canvas实现,这样只需要上传一张图片呢。
1 @Controller 2 @RequestMapping(value="upload") 3 public class FileUploadController { 4 5 @RequestMapping(value="image") 6 public void fileUpload(MultipartHttpServletRequest request, HttpServletResponse response) { 7 Map<String, Object> resultMap = new HashMap<String, Object>(); 8 String newRealFileName = null; 9 try { 10 MultipartHttpServletRequest multipartRequest = request; 11 CommonsMultipartFile file = (CommonsMultipartFile) multipartRequest.getFile("file"); 12 // 获得文件名: 13 String realFileName = file.getOriginalFilename(); 14 if(file.getSize()/1024>=5*1024){ 15 resultMap.put("status", 1); 16 resultMap.put("message", "图片不能大于5M"); 17 }else{ 18 System.out.println("获得文件名:" + realFileName); 19 newRealFileName = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + realFileName.substring(realFileName.indexOf(".")); 20 // 获取路径 21 String ctxPath = request.getSession().getServletContext().getRealPath("//") + "//temp//"; 22 System.out.println(ctxPath); 23 // 创建文件 24 File dirPath = new File(ctxPath); 25 if (!dirPath.exists()) { 26 dirPath.mkdir(); 27 } 28 File uploadFile = new File(ctxPath +"now"+ newRealFileName); 29 FileCopyUtils.copy(file.getBytes(), uploadFile); 30 // request.setAttribute("files", loadFiles(request)); 31 32 //获取图片的宽度和高度 33 InputStream is = new FileInputStream(ctxPath +"now"+ newRealFileName); 34 BufferedImage buff = ImageIO.read(is); 35 int realWidth=buff.getWidth(); 36 int realHeight = buff.getHeight(); 37 is.close(); 38 //接下来将图片的四个旋转进行保存 39 //1.向左旋转90度 40 String onenewRealFileName = "one"+newRealFileName; 41 Thumbnails.of(uploadFile).size(realWidth, realHeight).rotate(90).toFile(new File(ctxPath+onenewRealFileName) 42 ); 43 //2.向左旋转180度 44 String twonewRealFileName="two"+newRealFileName; 45 Thumbnails.of(uploadFile).size(realWidth, realHeight).rotate(180).toFile(new File(ctxPath+twonewRealFileName) 46 ); 47 //3.向左旋转270度 48 String threenewRealFileName="thr"+newRealFileName; 49 Thumbnails.of(uploadFile).size(realWidth, realHeight).rotate(270).toFile(new File(ctxPath+threenewRealFileName) 50 ); 51 resultMap.put("status", 0); 52 resultMap.put("fileName", "now"+newRealFileName); 53 } 54 } catch (Exception e) { 55 resultMap.put("status", 1); 56 resultMap.put("message", "图片上传出错"); 57 System.out.println(e); 58 } finally { 59 PrintWriter out = null; 60 try { 61 out = response.getWriter(); 62 } catch (IOException e) { 63 e.printStackTrace(); 64 } 65 //必须设置字符编码,否则返回json会乱码 66 response.setContentType("text/html;charset=UTF-8"); 67 out.write(JSONSerializer.toJSON(resultMap).toString()); 68 out.flush(); 69 out.close(); 70 } 71 }
80}
2.图片的展示
图片上传成功后,然后讲一个json字符串返回到前端,其中包括了上传完后图片的路径。下面就需要展示图片。想一下,上传的图片大小不一,DIV如何才能包住图片呢?如果在<img>设置图片的宽度=div宽度,图片的高度=div高度,这样的话,对图片会进行不等比例拉伸或者缩小,这样图片就完全变形,看上去肯定很不美观。我们需要做的就是,让图片按照原来的比例进行缩放。这样,图片看上去没有拉伸的那种效果。
这种请款下,就得分四种情况讨论:
- 图片的原始宽和高都比DIV的宽和高小,那么这种情况下,图片需要在水平和垂直上居中显示
我是通过margin-top属性和margin-left属性来对图片进行设置,以使其居中。margin-top:(DIV高-图片高)/2 margin-left:(DIV宽-图片宽)/2
- 图片的原始宽<DIV的宽,图片的原始高>DIV的高,这种情况下,计算图片的原始宽高比compareImage,然后,让图片的高=DIV高度,再根据图片的原始宽高比,算出现在图片的宽,也就是图片缩放的效果;
这种情况下margin-top:0 margin-left:(DIV宽度-图片现宽)/2让图片居中;
- 图片的原始宽>DIV的宽,图片的原始高<DIV的高,这种情况下,计算图片的原始宽高比compareImage,然后,让图片的宽=DIV宽,再根据图片的原始宽高比,算出现在图片的高,也就是图片缩放的效果;后面两种效果图就不展示了
- 图片的原始宽>DIV的宽,图片的原始高>DIV的高,这种情况比较复杂,因为你要比较谁最后溢出,例如,因为图片的宽和高都溢出,所以,如果图片你的宽==DIV的宽度,那么按照比例缩放后,你必须保证图片现高<=DIV的高度。也就是必须保证图片的宽和高都不溢出。
好,既然长宽都固定好了,肯定图片不会溢出DIV,那么现在就需要在DIV中展示效果,这里用到了"jquery.Jcrop.min.js"这个插件来实现,图片加载成功的时候,裁剪框也跟着显示出来。
也许你不愿意去看懂"jquery.Jcrop.min.js"的代码,说实话,我也不愿意去看,用法:
- 初始化一个对象,这个对象在我的"jQuery.UtrialAvatarCutter.js"中有说明,下面有提供下载地址,以及源码
这里的picture_original指的是图片外面DIV的id ,这里的resourceImage是图片的id
这里的bigImage 和 smallImage指的是头像预览两个DIV框
var cutter = new jQuery.UtrialAvatarCutter( { //主图片所在容器ID content : "picture_original,resourceImage", //缩略图配置,ID:所在容器ID;width,height:缩略图大小 purviews : [{id:"bigImage",width:100,height:100},{id:"smallImage",width:50,height:50}], } );
- 在图片加载完成的时候,这里,也就是异步提交完成的时候,进行的处理,在上面异步提交图片的时候有说明,success()里面的逻辑代码没有写,下面就是要处理的业务逻辑,也就是设置图片的宽和高,显示裁剪框
1 success : function(data) { 2 3 $(".page-left-center").hide(); 4 $(".page-left-center1").show(); 5 var imagepath="temp/"+data.fileName; 6 $("#resourceImage").attr("src", imagepath).load(function(){ 7 //获取图片的真实大小: 8 var realWidth; 9 var realHeight; 10 realWidth = parseInt(this.width); 11 realHeight =parseInt(this.height); 12 rWidth=realWidth; 13 rHeight=realHeight; 14 realCompare=parseFloat(realWidth)/parseFloat(realHeight); 15 //让图片适应div大小 16 console.info("图片的真实宽度:"+realWidth); 17 console.info("图片的真实高度:"+realHeight); 18 if(realWidth<divWidth){ 19 if(realHeight<divHeight){ 20 console.info("进入了宽小,高小"); 21 imageWidthAfter=realWidth; 22 imageHeightAfter=realHeight; 23 shengHeight=parseInt((divHeight-imageHeightAfter)/2); 24 $("#resourceImage").css("margin-top",shengHeight); 25 shengWidth=parseInt((divWidth-imageWidthAfter)/2); 26 $("#resourceImage").css("margin-left",shengWidth); 27 suoCompare=1; 28 }else{ 29 console.info("进入了宽小,高大"); 30 imageHeightAfter=divHeight; 31 imageWidthAfter=imageHeightAfter*realCompare; 32 shengWidth=parseInt((divWidth-imageWidthAfter)/2); 33 $("#resourceImage").css("margin-left",shengWidth); 34 shengHeight=0; 35 suoCompare=parseFloat(realHeight)/parseFloat(divHeight); 36 } 37 }else{ 38 if(realHeight<divHeight){ 39 console.info("进入了宽大,高小"); 40 imageWidthAfter=divWidth; 41 imageHeightAfter=parseFloat(divWidth/parseFloat(realCompare)); 42 shengHeight=parseInt((divHeight-imageHeightAfter)/2); 43 $("#resourceImage").css("margin-top",shengHeight); 44 shengWidth=0; 45 suoCompare=parseFloat(realWidth)/parseFloat(divWidth); 46 }else{ 47 console.info("进入了高大,宽大但处理后不满"); 48 //刚开始假如高满宽不满,那么,根据高得出宽 49 imageHeightAfter=divHeight; 50 imageWidthAfter=imageHeightAfter*realCompare; 51 //处理完后,宽还是溢出的话,说明以宽为基准 52 console.info("处理后的宽度:"+imageWidthAfter); 53 if(imageWidthAfter>divWidth){ 54 console.info("进入到了宽大,高大但高不满"); 55 imageWidthAfter=divWidth; 56 imageHeightAfter=parseFloat(divWidth/parseFloat(realCompare)); 57 shengHeight=parseInt((divHeight-imageHeightAfter)/2); 58 $("#resourceImage").css("margin-top",shengHeight); 59 shengWidth=0; 60 suoCompare=parseFloat(realWidth)/parseFloat(divWidth); 61 }else{ 62 shengWidth=parseInt((divWidth-imageWidthAfter)/2); 63 $("#resourceImage").css("margin-left",shengWidth); 64 shengHeight=0; 65 suoCompare=parseFloat(realHeight)/parseFloat(divHeight); 66 } 67 } 68 } 69 70 $("#resourceImage").show(); 71 $(".jcrop-holder").remove(); 72 console.info("shengHeight:"+shengHeight); 73 $("#resourceImage").width(imageWidthAfter); 74 $("#resourceImage").height(imageHeightAfter); 75 cutter.init(); 76 $(".jcrop-holder").css("margin-top",shengHeight); 77 $(".jcrop-holder").css("margin-left",shengWidth); 78 $(".jcrop-holder img").css("margin-top","0px"); 79 $(".jcrop-holder img").css("margin-left","0px"); 80 }); 81 $("#rechose").show(); 82 $(".btsave").attr("disabled",false); 83 $(".btsave").addClass("btsaveclick"); 84 } 85 };
3.图片的旋转
这块实现的不是特别理想,前面也说过了,就是刚开始上传的时候,上传四张,0度,90度,180度,270度。最近查看了HTML5的canvas,觉得这个挺好的。以后会试着通过HTML5来实现。旋转的实现很简单,每点击的时候,设置一个表示step 然后,通过switch进行判断,如果switch=0,则设置image的src=0度的图片,一次往后推。设置完成后,然后通过 cutter.init();重新显示裁剪框以及图片!
4.图片的裁剪
图片的裁剪原理,其实就是在原有图片的基础上,从(x,y)点开始,截取长度为宽度为w,高度为h的区域。
注意:由于显示的时候,不让图片超出DIV,所以,对图片进行了缩放,所以一这里你的x,y,w,h是还原后的。
1 $(".btsave").click(function(){ 2 var data = cutter.submit(); 3 var trueX=parseInt(parseFloat(data.x)*suoCompare); 4 var trueY=parseInt(parseFloat(data.y)*suoCompare); 5 var trueW=parseInt(parseFloat(data.w)*suoCompare); 6 var trueH=parseInt(parseFloat(data.h)*suoCompare); 7 var message=trueX+","+trueY+","+trueW+","+trueH+","+data.s; 8 console.info(message); 9 $("#cutHeader").val(message); 10 $("#formSubmit").submit(); 11});
如上所示,提交到后台,在后台进行处理,后台逻辑如下:
1 @RequestMapping(value="cutImage") 2 public ModelAndView cutImage(HttpServletRequest request, HttpServletResponse response) throws IOException { 3 String message=request.getParameter("header"); 4 int x=Integer.parseInt(message.split(",")[0]); 5 int y=Integer.parseInt(message.split(",")[1]); 6 int w=Integer.parseInt(message.split(",")[2]); 7 int h=Integer.parseInt(message.split(",")[3]); 8 String src=message.split(",")[4].split("\\?")[0]; 9 // 10 // 获取路径 11 String ctxPath = request.getSession().getServletContext().getRealPath("//") ; 12 System.out.println(ctxPath); 13 // 创建文件 14 src =ctxPath+"\\"+src; 15 String last=src.substring(src.lastIndexOf (".")); 16 File dirPath = new File(ctxPath+"\\header"); 17 if (!dirPath.exists()) { 18 dirPath.mkdir(); 19 } 20 String dest = ctxPath+"\\header\\"+new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())+last; 21 String headerurl="header/"+new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())+last; 22 boolean isSuccess=cutImageHeader(src,dest,x,y,w,h); 23 if(isSuccess){ 24 return new ModelAndView("showHeader","image",headerurl); 25 }else{ 26 return new ModelAndView("changeHeader"); 27 } 28 29 } 30 31 public static boolean cutImageHeader(String src,String dest,int x,int y,int w,int h) throws IOException{ 32 String last=src.substring(src.lastIndexOf (".")+1); 33 System.out.println("*****"+last); 34 Iterator iterator = ImageIO.getImageReadersByFormatName(last); 35 ImageReader reader = (ImageReader)iterator.next(); 36 InputStream in=new FileInputStream(src); 37 ImageInputStream iis = ImageIO.createImageInputStream(in); 38 reader.setInput(iis, true); 39 ImageReadParam param = reader.getDefaultReadParam(); 40 Rectangle rect = new Rectangle(x, y, w,h); 41 System.out.println("x:"+x+";y:"+y+";w:"+w+";h:"+h); 42 param.setSourceRegion(rect); 43 BufferedImage bi = reader.read(0,param); 44 boolean isSuccess=ImageIO.write(bi, last, new File(dest)); 45 return isSuccess; 46 }