最近在做的這個項目非常重視頁面的用戶效果體驗,要求用戶注冊時上傳的頭像自動生成圓形,這樣做一來是為了好看,二來是讓公司安卓、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,注釋寫的很詳細,代碼如下:
1 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我將寫在下一篇文章里。