plupload:Springmvc使用plupload上傳頭像,並自動剪切圓形


  最近在做的這個項目非常重視頁面的用戶效果體驗,要求用戶注冊時上傳的頭像自動生成圓形,這樣做一來是為了好看,二來是讓公司安卓、iOS那邊可以直接使用用戶頭像,不用剪切。該功能我使用了plupload上傳插件,然后通過查閱資料寫了一個圓形切割的Java工具類,下面記錄一下完成的過程:

一、plupload的特性

  Plupload這個JavaScript控件可以讓你選擇Adobe Flash、Google Gears、HTML5、Microsoft Silverlight、Yahoo BrowserPlus或正常表單Form等多種方法進行文件上傳。Plupload還提供其它功能包括:上傳進度提醒、圖片縮小、多文件上傳,拖拽文件 到上傳控件,文件類型過濾和Chunked上傳等。這些功能在不同的上傳方式中支持情況會受到限制。

二、plupload的使用

  1. 下載plupload:http://www.plupload.com/download;
  2. 統一寫一個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 }
    View Code
  3. 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;
        }
  4. 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 }
View Code

注:1、jsp頁面處理的代碼中包含少量無關代碼,我將繼續修改成通用版。2、此功能第一步是使用Jcrop組件實現圖片預覽,具體如何使用的Jcrop我將寫在下一篇文章里。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM