最近在做的这个项目非常重视页面的用户效果体验,要求用户注册时上传的头像自动生成圆形,这样做一来是为了好看,二来是让公司安卓、iOS那边可以直接使用用户头像,不用剪切。该功能我使用了plupload上传插件,然后通过查阅资料写了一个圆形切割的Java工具类,下面记录一下完成的过程:
一、plupload的特性
Plupload这个JavaScript控件可以让你选择Adobe Flash、Google Gears、HTML5、Microsoft Silverlight、Yahoo BrowserPlus或正常表单Form等多种方法进行文件上传。Plupload还提供其它功能包括:上传进度提醒、图片缩小、多文件上传,拖拽文件 到上传控件,文件类型过滤和Chunked上传等。这些功能在不同的上传方式中支持情况会受到限制。
二、plupload的使用
- 下载plupload:http://www.plupload.com/download;
- 统一写一个plupload的操作类PluploadUtil.java,注释写的很详细,代码如下:
View Code1 import java.io.BufferedOutputStream; 2 import java.io.File; 3 import java.io.FileOutputStream; 4 import java.io.IOException; 5 import java.io.InputStream; 6 import java.io.OutputStream; 7 import java.util.Iterator; 8 import java.util.List; 9 10 import org.springframework.util.MultiValueMap; 11 import org.springframework.web.multipart.MultipartFile; 12 import org.springframework.web.multipart.MultipartHttpServletRequest; 13 14 import com.law.bean.Plupload; 15 16 /** 17 * Plupload是一个上传插件。 18 * 上传原理为单个文件依次发送至服务器. 19 * 上传打文件时可以将其碎片化上传。但是一般情况下,不会这样做, 20 * 所以这里更多的是处理普通文件的批量上传。 21 * 这里主要处理文件上传 22 */ 23 public class PluploadUtil { 24 private static final int BUF_SIZE = 2 * 1024; 25 /**上传失败响应的成功状态码*/ 26 public static final String RESP_SUCCESS = "{\"jsonrpc\" : \"2.0\", \"result\" : \"success\", \"id\" : \"id\"}"; 27 /**上传失败响应的失败状态码*/ 28 public static final String RESP_ERROR = "{\"jsonrpc\" : \"2.0\", \"error\" : {\"code\": 101, \"message\": \"Failed to open input stream.\"}, \"id\" : \"id\"}"; 29 30 /** 31 * 用于Plupload插件的文件上传,自动生成唯一的文件保存名 32 * @param plupload - 存放上传所需参数的bean 33 * @param dir - 保存目标文件目录 34 * @throws IllegalStateException 35 * @throws IOException 36 */ 37 public static String upload(Plupload plupload, File dir) throws IllegalStateException, IOException { 38 //生成唯一的文件名 39 String filename = "" + System.currentTimeMillis() + plupload.getName(); 40 upload(plupload, dir, filename); 41 return filename; 42 } 43 44 /** 45 * 用于Plupload插件的文件上传 46 * @param plupload - 存放上传所需参数的bean 47 * @param dir - 保存目标文件目录 48 * @param filename - 保存的文件名 49 * @throws IllegalStateException 50 * @throws IOException 51 */ 52 public static void upload(Plupload plupload, File dir, String filename) throws IllegalStateException, IOException { 53 int chunks = plupload.getChunks(); //获取总的碎片数 54 int chunk = plupload.getChunk(); //获取当前碎片(从0开始计数) 55 56 System.out.println(plupload.getMultipartFile() + "----------"); 57 58 MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) plupload.getRequest(); 59 MultiValueMap<String, MultipartFile> map = multipartRequest.getMultiFileMap(); 60 61 if(map != null) { 62 if (!dir.exists()) dir.mkdirs(); //如果目标文件夹不存在则创建新的文件夹 63 64 //事实上迭代器中只存在一个值,所以只需要返回一个值即可 65 Iterator<String> iter = map.keySet().iterator(); 66 while(iter.hasNext()) { 67 String str = (String) iter.next(); 68 List<MultipartFile> fileList = map.get(str); 69 for(MultipartFile multipartFile : fileList) { 70 //因为只存在一个值,所以最后返回的既是第一个也是最后一个值 71 plupload.setMultipartFile(multipartFile); 72 73 //创建新目标文件 74 File targetFile = new File(dir.getPath()+ "/" + filename); 75 76 //当chunks>1则说明当前传的文件为一块碎片,需要合并 77 if (chunks > 1) { 78 //需要创建临时文件名,最后再更改名称 79 File tempFile = new File(dir.getPath()+ "/" + multipartFile.getName()); 80 //如果chunk==0,则代表第一块碎片,不需要合并 81 saveUploadFile(multipartFile.getInputStream(), tempFile, chunk == 0 ? false : true); 82 83 //上传并合并完成,则将临时名称更改为指定名称 84 if (chunks - chunk == 1) { 85 tempFile.renameTo(targetFile); 86 } 87 88 } else { 89 //否则直接将文件内容拷贝至新文件 90 multipartFile.transferTo(targetFile); 91 } 92 } 93 } 94 } 95 96 } 97 98 /** 99 * 保存上传文件,兼合并功能 100 */ 101 private static void saveUploadFile(InputStream input, File targetFile, boolean append) throws IOException { 102 OutputStream out = null; 103 try { 104 if (targetFile.exists() && append) { 105 out = new BufferedOutputStream(new FileOutputStream(targetFile, true), BUF_SIZE); 106 } else { 107 out = new BufferedOutputStream(new FileOutputStream(targetFile), BUF_SIZE); 108 } 109 110 byte[] buffer = new byte[BUF_SIZE]; 111 int len = 0; 112 //写入文件 113 while ((len = input.read(buffer)) > 0) { 114 out.write(buffer, 0, len); 115 } 116 } catch (IOException e) { 117 throw e; 118 } finally { 119 //关闭输入输出流 120 if (null != input) { 121 try { 122 input.close(); 123 } catch (IOException e) { 124 e.printStackTrace(); 125 } 126 } 127 if (null != out) { 128 try { 129 out.close(); 130 } catch (IOException e) { 131 e.printStackTrace(); 132 } 133 } 134 } 135 } 136 137 /** 138 * 判断是否全部上传完成 139 * 碎片需合并后才返回真 140 */ 141 public static boolean isUploadFinish(Plupload plupload) { 142 return (plupload.getChunks() - plupload.getChunk() == 1); 143 } 144 145 }
- controller中的后台处理:
/**上传处理方法*/ @RequestMapping(value="/upload", method = RequestMethod.POST) @ResponseBody public Map<String,String> upload(Plupload plupload,HttpServletRequest request, HttpServletResponse response) { //输出信息 System.out.println(plupload.getChunk() + "===" + plupload.getName() + "---" + plupload.getChunks()); System.out.println("operate="+operate); String filename =null; plupload.setRequest(request); SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM"); String ymd = sdf.format(new Date()); // 文件保存目录路径 String savePath = plupload.getRequest().getSession().getServletContext().getRealPath("/") + FileDir; // 临时文件目录 String tempPath = plupload.getRequest().getSession().getServletContext().getRealPath("/") + TempFileDir; Map<String,String> msg = new HashMap<String,String>(); savePath += "photo/"; tempPath += "photo/"; // 创建文件夹 File dirFile = new File(savePath); if (!dirFile.exists()) { dirFile.mkdirs(); } // 创建临时文件夹 File dirTempFile = new File(tempPath); if (!dirTempFile.exists()){ dirTempFile.mkdirs(); } System.out.println(dirFile.getPath()); try { //上传文件 filename= PluploadUtil.upload(plupload, dirFile); //判断文件是否上传成功(被分成块的文件是否全部上传完成) if (PluploadUtil.isUploadFinish(plupload)) { System.out.println(plupload.getName() + "----"); System.out.println(savePath+"&&"+filename + "====="); //限制图片大小不能小于300*300 BufferedImage bi = ImageIO.read(new FileInputStream(new File(savePath+filename))); System.out.println("width="+bi.getWidth()); System.out.println("height="+bi.getHeight()); if(bi.getWidth()<200||bi.getHeight()<200){ msg.put("fail", "True"); msg.put("error","图片尺寸不符合要求,大于300*300"); }else{ //将图片等比压缩为300*300大小的图片 IconCompressUtil.compressImg(new File(savePath+filename), 300, new File(savePath+filename)); msg.put("success", "True"); msg.put("filename", filename); msg.put("filepath", savePath+"temp"+filename); System.out.println("头像保存位置="+savePath+"temp"+filename); msg.put("width", bi.getWidth()+""); msg.put("height", bi.getHeight()+""); } }else{ msg.put("fail", "True"); } } catch (IllegalStateException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return msg; }
- jsp页面
var uploader = new plupload.Uploader({ // General settings runtimes : 'html5,flash,silverlight,html4', browse_button : 'pickfiles', // you can pass in id... multi_selection: false, container: document.getElementById('container'), url : '<%=path%>/upload.do?operate=1', chunk_size : '1mb', unique_names : true, // Resize images on client-side if we can //resize : { width : 320, height : 240, quality : 90 }, filters : { max_file_size : '4mb', // Specify what files to browse for mime_types: [ {title : "图片文件", extensions : "jpg,jpeg,gif,png,bmp"} ] }, flash_swf_url : '<%=path%>/plupload/js/Moxie.swf', silverlight_xap_url : '<%=path%>/plupload/js/Moxie.xap', init: { FilesAdded: function (up, files) { var count = uploader.files.length; if (count > 1) { art.dialog({ title: '操作提示', width: '24em', height: '10em', content: "一次最多只能上传一张图片", icon: 'error', yesFn: function () { return true; } }); } else if (count == 0) { art.dialog({ title: '操作提示', width: '24em', height: '10em', content: "没有要上传的图片", icon: 'error', yesFn: function () { return true; } }); } else { uploader.start(); } return false; }, UploadProgress: function (up, file) { $("#uploadpercent").show().addClass("cutbox").addClass("cutpic").find("img").eq(0).show().attr("src", "<%=path%>/images/loading_upload.gif"); $("#fileName").val(file.name); imgcontainer.hide(); }, UploadComplete: function (up, file) { if (uploader.total.failed > 0) { //if (confirm("对不起," + uploader.total.failed + "个文件上传失败,是否重新上传?")) { $("#uploadpercent").hide().removeClass("cutbox").removeClass("cutpic").find("img").eq(0).hide().attr("src", ""); alert("UploadComplete"); imgcontainer.show(); //uploader.start(); //} else { uploader.splice(); uploader.refresh(); //} } else if (uploader.files.length == (uploader.total.uploaded + uploader.total.failed)) { $("#uploadpercent").hide().removeClass("cutbox").removeClass("cutpic").find("img").eq(0).hide().attr("src", ""); imgcontainer.show(); } }, FileUploaded: function (up, file, obj) { var result = eval('(' + obj.response + ')'); if (result.success == "True") { uploader.splice(); uploader.refresh(); $("#headVal").val(result.filename); $("#filepath").val(result.filepath); var imgurl = "<%=path%>/uploadfile/photo/" + result.filename; var maxVal = 0; if (result.width > result.height) { maxVal = result.height; } else { maxVal = result.width; } alert("maxVal="+maxVal); alert("result.width="+result.width); alert("result.height="+result.height); $("#maxVal").val(maxVal); $("#owidth").val(result.width);//result.width $("#oheight").val(result.height);//result.height $("#oper").show(); imgcontainer.addClass("cutpic"); cutter.reload(imgurl,result.width,result.height); alert("FileUploaded"); } else { alert(result.error); $("#uploadpercent").hide().removeClass("cutbox").removeClass("cutpic").find("img").eq(0).hide().attr("src", ""); imgcontainer.show(); uploader.total.failed += 1; uploader.splice(); uploader.refresh(); art.dialog({ title: '操作提示', width: '24em', height: '10em', content: result.filename, icon: 'error', yesFn: function () { return true; } }); } } } }); uploader.init();
三、圆形切割类CircleCut.java:
public class CircleCut { String srcFile ; String newFile; public String getSrcFile() { return srcFile; } public void setSrcFile(String srcFile) { this.srcFile = srcFile; } public String getNewFile() { return newFile; } public void setNewFile(String newFile) { this.newFile = newFile; } public static boolean ImgCircleCut(String srcFile,String newFile){ try { BufferedImage bi1 = ImageIO.read(new File(srcFile)); // 根据需要是否使用 BufferedImage.TYPE_INT_ARGB BufferedImage bi2 = new BufferedImage(bi1.getWidth(), bi1.getHeight(), BufferedImage.TYPE_INT_ARGB); Ellipse2D.Double shape = new Ellipse2D.Double(0, 0, bi1.getWidth(), bi1 .getHeight()); Graphics2D g2 = bi2.createGraphics(); AlphaComposite ac = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.9f); g2.setComposite(ac); g2.setBackground(new Color(22,2,2,0)); //-1000000:在编写这个类时测试发现将此值设为极小值则可切出圆形 g2.fill3DRect(-1000000, 200, 180, 80,false); //g2.fill(new Rectangle(bi2.getWidth(), bi2.getHeight())); g2.setClip(shape); // 使用 setRenderingHint 设置抗锯齿 g2.drawImage(bi1, 0, 0, null); g2.dispose(); ImageIO.write(bi2, "png", new File(newFile)); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); return false; } return true; } }
四、由于以上圆形切割类CircleCut.java最好是剪切尺寸比例为1:1的图片,偏离这个比例则会成为椭圆。那么为解决这一问题,我们在上传头像后在后台使用图片处理工具类IconCompressUtil.java切割成固定1:1尺寸的图片,再调用CircleCut.java。以下为IconCompressUtil.java:
1 public class IconCompressUtil { 2 public String path = ""; 3 4 public IconCompressUtil(String path) { 5 this.path = path; 6 } 7 8 public void change(int size) { 9 compressImg(new File(path), size, null); 10 } 11 12 /** 13 * @param oldfile 14 * @param size 15 * @param newfile 16 * @return 17 * @描述 —— 将oldfile的图片文件等比例压缩为size的newfile文件 18 */ 19 20 public static File compressImg(File oldfile, int size, File newfile) { 21 if(!newfile.exists()) 22 try { 23 newfile.createNewFile(); 24 } catch (IOException e1) { 25 // TODO Auto-generated catch block 26 //e1.printStackTrace(); 27 System.out.println("无法创建文件!!!"); 28 return null; 29 } 30 BufferedImage bi; 31 try { 32 System.out.println("正在压缩:" + oldfile.getName()); 33 bi = ImageIO.read(new FileInputStream(oldfile)); 34 int width = bi.getWidth(); 35 int height = bi.getHeight(); 36 if (width > size || height > size) { 37 Image image; 38 if (width > height) { 39 height = (int) (bi.getHeight() / (bi.getWidth() * 1d) * size); 40 image = bi.getScaledInstance(size, height, 41 Image.SCALE_DEFAULT); 42 } else { 43 width = (int) (bi.getWidth() / (bi.getHeight() * 1d) * size); 44 image = bi.getScaledInstance(width, size, 45 Image.SCALE_DEFAULT); 46 } 47 ImageIO.write(toBufferedImage(image), "png", 48 new FileOutputStream(newfile)); 49 System.out.println("压缩完成:" + newfile.getName()); 50 return newfile; 51 } else { 52 System.out.println("无须压缩:" + oldfile.getName()); 53 return oldfile; 54 } 55 } catch (Exception e) { 56 e.printStackTrace(); 57 } 58 return null; 59 } 60 61 public static BufferedImage toBufferedImage(Image image) { 62 if (image instanceof BufferedImage) { 63 return (BufferedImage) image; 64 } 65 image = new ImageIcon(image).getImage(); 66 BufferedImage bimage = null; 67 GraphicsEnvironment ge = GraphicsEnvironment 68 .getLocalGraphicsEnvironment(); 69 try { 70 int transparency = Transparency.TRANSLUCENT; 71 GraphicsDevice gs = ge.getDefaultScreenDevice(); 72 GraphicsConfiguration gc = gs.getDefaultConfiguration(); 73 bimage = gc.createCompatibleImage(image.getWidth(null), image 74 .getHeight(null), transparency); 75 } catch (HeadlessException e) { 76 } 77 if (bimage == null) { 78 int type = BufferedImage.TYPE_INT_RGB; 79 bimage = new BufferedImage(image.getWidth(null), image 80 .getHeight(null), type); 81 } 82 Graphics g = bimage.createGraphics(); 83 g.drawImage(image, 0, 0, null); 84 g.dispose(); 85 return bimage; 86 } 87 88 /** 89 * @return 90 * @描述 —— 生成随机名字,不可能重复(用于文件的命名) 91 */ 92 public static String getRandomName() { 93 Random r = new Random(); 94 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmssSSS"); 95 StringBuffer sb = new StringBuffer(); 96 sb.append(r.nextInt(100)); 97 sb.append(r.nextInt(100)); 98 sb.append("_"); 99 sb.append(sdf.format(new Date())); 100 sb.append("_"); 101 sb.append(r.nextInt(100)); 102 sb.append(r.nextInt(100)); 103 return sb.toString(); 104 } 105 106 /** 107 * @param inputFile源文件 108 * @param outFile生成文件 109 * @param width指定宽度 110 * @param height指定高度 111 * @param proportion是否等比例操作 112 * @return 113 * @描述 —— 是否等比例缩放图片 114 */ 115 public static boolean compressPic(String inputFile, String outFile, 116 int width, int height, boolean proportion) { 117 try { 118 // 获得源文件 119 File file = new File(inputFile); 120 if (!file.exists()) { 121 return false; 122 } 123 Image img = ImageIO.read(file); 124 // 判断图片格式是否正确 125 if (img.getWidth(null) == -1) { 126 return false; 127 } else { 128 int newWidth; 129 int newHeight; 130 // 判断是否是等比缩放 131 if (proportion == true) { 132 // 为等比缩放计算输出的图片宽度及高度 133 double rate1 = ((double) img.getWidth(null)) 134 / (double) width + 0.1; 135 double rate2 = ((double) img.getHeight(null)) 136 / (double) height + 0.1; 137 // 根据缩放比率大的进行缩放控制 138 double rate = rate1 > rate2 ? rate1 : rate2; 139 newWidth = (int) (((double) img.getWidth(null)) / rate); 140 newHeight = (int) (((double) img.getHeight(null)) / rate); 141 } else { 142 newWidth = width; // 输出的图片宽度 143 newHeight = height; // 输出的图片高度 144 } 145 146 // 如果图片小于目标图片的宽和高则不进行转换 147 /* 148 * if (img.getWidth(null) < width && img.getHeight(null) < 149 * height) { newWidth = img.getWidth(null); newHeight = 150 * img.getHeight(null); } 151 */ 152 BufferedImage tag = new BufferedImage((int) newWidth, 153 (int) newHeight, BufferedImage.TYPE_INT_RGB); 154 155 // Image.SCALE_SMOOTH 的缩略算法 生成缩略图片的平滑度的,优先级比速度高 生成的图片质量比较好 但速度慢 156 tag.getGraphics().drawImage( 157 img.getScaledInstance(newWidth, newHeight, 158 Image.SCALE_SMOOTH), 0, 0, null); 159 FileOutputStream out = new FileOutputStream(outFile); 160 // JPEGImageEncoder可适用于其他图片类型的转换 161 JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out); 162 encoder.encode(tag); 163 out.close(); 164 } 165 } catch (IOException ex) { 166 ex.printStackTrace(); 167 } 168 return true; 169 } 170 171 /** 172 * @param srcFile源文件 173 * @param outFile输出文件 174 * @param x坐标 175 * @param y坐标 176 * @param width宽度 177 * @param height高度 178 * @return 179 * @描述 —— 裁剪图片 180 */ 181 public static boolean cutPic(String srcFile, String outFile, int x, int y, 182 int width, int height) { 183 FileInputStream is = null; 184 ImageInputStream iis = null; 185 try { 186 // 如果源图片不存在 187 if (!new File(srcFile).exists()) { 188 return false; 189 } 190 191 // 读取图片文件 192 is = new FileInputStream(srcFile); 193 194 // 获取文件格式 195 String ext = srcFile.substring(srcFile.lastIndexOf(".") + 1); 196 197 // ImageReader声称能够解码指定格式 198 Iterator<ImageReader> it = ImageIO.getImageReadersByFormatName(ext); 199 ImageReader reader = it.next(); 200 201 // 获取图片流 202 iis = ImageIO.createImageInputStream(is); 203 204 // 输入源中的图像将只按顺序读取 205 reader.setInput(iis, true); 206 207 // 描述如何对流进行解码 208 ImageReadParam param = reader.getDefaultReadParam(); 209 210 // 图片裁剪区域 211 Rectangle rect = new Rectangle(x, y, width, height); 212 213 // 提供一个 BufferedImage,将其用作解码像素数据的目标 214 param.setSourceRegion(rect); 215 216 // 使用所提供的 ImageReadParam 读取通过索引 imageIndex 指定的对象 217 BufferedImage bi = reader.read(0, param); 218 219 // 保存新图片 220 File tempOutFile = new File(outFile); 221 if (!tempOutFile.exists()) { 222 tempOutFile.mkdirs(); 223 } 224 ImageIO.write(bi, ext, new File(outFile)); 225 return true; 226 } catch (Exception e) { 227 e.printStackTrace(); 228 return false; 229 } finally { 230 try { 231 if (is != null) { 232 is.close(); 233 } 234 if (iis != null) { 235 iis.close(); 236 } 237 } catch (IOException e) { 238 e.printStackTrace(); 239 return false; 240 } 241 } 242 } 243 244 /** 245 * @param doubleValue 246 * @return 247 * @描述 —— 将浮点型数据保留整数位转换成int型 248 */ 249 public static Integer getRoundIntFromDouble(Double doubleValue) { 250 return Integer.parseInt(String.valueOf(Math.round(doubleValue))); 251 } 252 }
注:1、jsp页面处理的代码中包含少量无关代码,我将继续修改成通用版。2、此功能第一步是使用Jcrop组件实现图片预览,具体如何使用的Jcrop我将写在下一篇文章里。
