通过Canvas及File API缩放并上传图片

原文地址:Resize an Image Using Canvas, Drag and Drop and the File API 

示例地址:Canvas Resize Demo 

原文作者:Dr. Tom Trenka
原文日期: 2013年8月6日
翻译日期: 2013年8月8日

[html]  view plain copy
  1. Tom Trenka 能为"我"的博客写一篇文章,对我来说是一个巨大的荣誉。Tom是Dojo框架的最初贡献者之一,也是我在SitePen公司的良师益友.我见证了他最顶级的天才能力,并且他总是第一个以前瞻性的解决方案预见了很多棘手的问题。他总是站在局外思考,打破常规但却又坚实可靠地解决边缘问题。本文就是一个完美的例证。  

最近我总是被问道要创造一个用户接口API,允许用户上传图片到服务器上(伴随其他的事情),并能在我们公司提供支持的大量网站的客户端上使用。通常来说这都是很容易的事情——创建一个form表单,添加一个file类型的input输入框,让用户从电脑里选择图片,并在form标签上设置enctype="multipart/form-data"表单属性,然后上传即可。非常简单,不是吗?事实上,这里有一个足够简单的例子; 点击进入 

但是如果你想要通过某些方式预先处理一下图片再上传,那该怎么办?比如说,你必须先压缩图片尺寸,或者需要图片只能是某些种类的格式,如 png 或者jpg,你怎么办?
用canvas来解决!

Canvas简介
canvas 是一个HTML5新增的DOM元素,允许用户在页面上直接地绘制图形,通常是使用JavaScript.而不同的格式标准也是不同的,比如SVG是光栅API(raster API) 而VML却是向量API(vector API).可以考虑使用Adobe Illustrator(矢量图)作图与使用 Adobe Photoshop (光栅图)作图的区别。
在canvas(画布)上能做的事情就是读取和渲染图像,并且允许你通过JavaScript操纵图像数据。已经有很多现存的文章来为你演示基本的图像处理——主要关注与各种不同的图像过滤技术( image filtering techniques)——但我们需要的仅仅是缩放图片并转换到特定的文件格式,而canvas完全可以做到这些事情。
我们假定的需求,比如图像高度不超过100像素,不管原始图像有多高。基本的代码如下所示:

[javascript]  view plain copy
  1.        // 参数,最大高度  
  2. var MAX_HEIGHT = 100;  
  3. // 渲染  
  4. function render(src){  
  5.     // 创建一个 Image 对象  
  6.     var image = new Image();  
  7.     // 绑定 load 事件处理器,加载完成后执行  
  8.     image.onload = function(){  
  9.         // 获取 canvas DOM 对象  
  10.         var canvas = document.getElementById("myCanvas");  
  11.         // 如果高度超标  
  12.         if(image.height > MAX_HEIGHT) {  
  13.             // 宽度等比例缩放 *=  
  14.             image.width *= MAX_HEIGHT / image.height;  
  15.             image.height = MAX_HEIGHT;  
  16.         }  
  17.         // 获取 canvas的 2d 环境对象,  
  18.         // 可以理解Context是管理员,canvas是房子  
  19.         var ctx = canvas.getContext("2d");  
  20.         // canvas清屏  
  21.         ctx.clearRect(0, 0, canvas.width, canvas.height);  
  22.         // 重置canvas宽高  
  23.         canvas.width = image.width;  
  24.         canvas.height = image.height;  
  25.         // 将图像绘制到canvas上  
  26.         ctx.drawImage(image, 0, 0, image.width, image.height);  
  27.         // !!! 注意,image 没有加入到 dom之中  
  28.     };  
  29.     // 设置src属性,浏览器会自动加载。  
  30.     // 记住必须先绑定事件,才能设置src属性,否则会出同步问题。  
  31.     image.src = src;  
  32. };  

在上面的例子中,你可以使用canvas 的 toDataURL() 方法获取图像的 Base64编码的值(可以类似理解为16进制字符串,或者二进制数据流).

注意: canvas 的 toDataURL()  获取的URL以字符串开头,有22个无用的数据 "data:image/png;base64,",需要在客户端或者服务端进行过滤.

原则上只要浏览器支持,URL地址的长度是没有限制的,而1024的长度限制,是老一代IE所独有的。

请问,如何获取我们需要的图像呢?
好孩子,很高兴你能这么问。你并不能通过File 输入框来直接处理,你从这个文件输入框元素所能获取的仅仅是用户所选择文件的path路径。按照常规想象,你可以通过这个path路径信息来加载图像,但是,在浏览器里面这是不现实的。(译者注:浏览器厂商必须保证自己的浏览器绝对安全,才能获得市场,至少避免媒体的攻击,如果允许这样做,那恶意网址可以通过拼凑文件路径来尝试获取某些敏感信息).
为了实现这个需求,我们可以使用HTML5的File API 来读取用户磁盘上的文件,并用这个file来作为图像的源(src,source).


File API简介
新的File API接口是在不违背任何安全沙盒规则下,读取和列出用户文件目录的一个途径—— 通过沙盒(sandbox)限制,恶意网站并不能将病毒写入用户磁盘,当然更不能执行。
我们要使用的文件读取对象叫做 FileReader,FileReader允许开发者读取文件的内容(具体浏览器的实现方式可能大不相同)。
假设我们已经获取了图像文件的path路径,那么依赖前面的代码,使用FileReader来加载和渲染图像就变得很容易了:

[javascript]  view plain copy
  1. // 加载 图像文件(url路径)  
  2. function loadImage(src){  
  3.     // 过滤掉 非 image 类型的文件  
  4.     if(!src.type.match(/image.*/)){  
  5.         if(window.console){  
  6.             console.log("选择的文件类型不是图片: ", src.type);  
  7.         } else {  
  8.             window.confirm("只能选择图片文件");  
  9.         }  
  10.         return;  
  11.     }  
  12.   
  13.     // 创建 FileReader 对象 并调用 render 函数来完成渲染.  
  14.     var reader = new FileReader();  
  15.     // 绑定load事件自动回调函数  
  16.     reader.onload = function(e){  
  17.         // 调用前面的 render 函数  
  18.         render(e.target.result);  
  19.     };  
  20.     // 读取文件内容  
  21.     reader.readAsDataURL(src);  
  22. };  
请问, 如何获取文件呢?
小白兔,要有耐心!我们的下一步就是获取文件,当然有好多方法可以实现啦。例如:你可以用文本框让用户输入文件路径,但很显然大多数用户都不是开发者,对输入什么值根本就不了解.
为了用户使用方便,我们采用 Drag and Drop API接口。

使用 Drag and Drop API
拖拽接口(Drag and Drop)非常简单——在大多数的DOM元素上,你都可以通过绑定事件处理器来实现.  只要用户从磁盘上拖动一个文件到dom对象上并放开鼠标,那我们就可以读取这个文件。代码如下:
[javascript]  view plain copy
  1. function init(){  
  2.     // 获取DOM元素对象  
  3.     var target = document.getElementById("drop-target");  
  4.     // 阻止 dragover(拖到DOM元素上方) 事件传递  
  5.     target.addEventListener("dragover"function(e){e.preventDefault();}, true);  
  6.     // 拖动并放开鼠标的事件  
  7.     target.addEventListener("drop"function(e){  
  8.         // 阻止默认事件,以及事件传播  
  9.         e.preventDefault();   
  10.         // 调用前面的加载图像 函数,参数为dataTransfer对象的第一个文件  
  11.         loadImage(e.dataTransfer.files[0]);  
  12.     }, true);  
  13.     var setheight = document.getElementById("setheight");  
  14.     var maxheight = document.getElementById("maxheight");  
  15.     setheight.addEventListener("click"function(e){  
  16.             //  
  17.             var value = maxheight.value;  
  18.             if(/^\d+$/.test(value)){  
  19.                 MAX_HEIGHT = parseInt(value);  
  20.             }  
  21.             e.preventDefault();  
  22.         },true);  
  23.     var btnsend = document.getElementById("btnsend");  
  24.     btnsend.addEventListener("click"function(e){  
  25.         //  
  26.         sendImage();  
  27.     },true);  
  28. };  
我们还可以做一些其他的处理,比如显示预览图。但如果不想压缩图片的话,那很可能没什么用。我们将采用Ajax通过HTTP 的post方式上传图片数据。下面的例子是使用Dojo框架来完成请求的,当然你也可以采用其他的Ajax技术来实现.

Dojo 代码如下:

[javascript]  view plain copy
  1. // 译者并不懂Dojo,所以将在后面附上jQuery的实现  
  2. //  Remember that DTK 1.7+ is AMD!  
  3. require(["dojo/request"], function(request){  
  4.     // 设置请求URL,参数,以及回调。  
  5.     request.post("image-handler.php", {  
  6.         data: {  
  7.             imageName: "myImage.png",  
  8.             imageData: encodeURIComponent(document.getElementById("canvas").toDataURL("image/png"))  
  9.         }  
  10.     }).then(function(text){  
  11.         console.log("The server returned: ", text);  
  12.     });  
  13. });  

jQuery 实现如下:

[javascript]  view plain copy
  1. // 上传图片,jQuery版  
  2. function sendImage(){  
  3.   // 获取 canvas DOM 对象  
  4.   var canvas = document.getElementById("myCanvas");  
  5.   // 获取Base64编码后的图像数据,格式是字符串  
  6.   // "data:image/png;base64,"开头,需要在客户端或者服务器端将其去掉,后面的部分可以直接写入文件。  
  7.   var dataurl = canvas.toDataURL("image/png");  
  8.   // 为安全 对URI进行编码  
  9.   // data%3Aimage%2Fpng%3Bbase64%2C 开头  
  10.   var imagedata =  encodeURIComponent(dataurl);  
  11.   //var url = $("#form").attr("action");  
  12.   // 1. 如果form表单不好处理,可以使用某个hidden隐藏域来设置请求地址  
  13.   // <input type="hidden" name="action" value="receive.jsp" />  
  14.   var url = $("input[name='action']").val();  
  15.   // 2. 也可以直接用某个dom对象的属性来获取  
  16.   // <input id="imageaction" type="hidden" action="receive.jsp">  
  17.   // var url = $("#imageaction").attr("action");  
  18.     
  19.   // 因为是string,所以服务器需要对数据进行转码,写文件操作等。  
  20.   // 个人约定,所有http参数名字全部小写  
  21.   console.log(dataurl);  
  22.   //console.log(imagedata);  
  23.   var data = {  
  24.             imagename: "myImage.png",  
  25.             imagedata: imagedata  
  26.         };  
  27.   jQuery.ajax( {  
  28.     url : url,  
  29.     data : data,  
  30.     type : "POST",  
  31.     // 期待的返回值类型  
  32.     dataType: "json",  
  33.     complete : function(xhr,result) {  
  34.         //console.log(xhr.responseText);  
  35.         var $tip2 = $("#tip2");  
  36.         if(!xhr){  
  37.             $tip2.text('网络连接失败!');  
  38.             return false;  
  39.         }  
  40.           
  41.         var text = xhr.responseText;  
  42.         if(!text){  
  43.             $tip2.text('网络错误!');  
  44.             return false;  
  45.         }  
  46.           
  47.         var json = eval("("+text+")");  
  48.         if(!json){  
  49.             $tip2.text('解析错误!');  
  50.             return false;  
  51.         } else {  
  52.             $tip2.text(json.message);  
  53.         }  
  54.         //console.dir(json);  
  55.         //console.log(xhr.responseText);  
  56.     }  
  57.   });  
  58. };  

OK,搞定!你还需要做的,就是创建一个只管的用户界面,并允许你控制图片的大小。上传到服务器端的数据,并不需要处理enctype为 multi-part/form-data 的情况,仅仅一个简单的POST表单处理程序就可以了.

[plain]  view plain copy
  1. Tom Trenka 博士简介  
  2. Tom Trenka ,软件开发者,明尼苏达州首府圣保罗的音乐及其他方面的专家。  
  3. DojoX项目的前lead,也是Dojo最早的代码贡献者之一,  
  4. Ai Media Group的高级开发工程师(Senior Software Developer),从事复杂数据分析程序的开发.  


好了,下面附上完整的代码示例:

[html]  view plain copy
  1. <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>  
  2. <%  
  3. String path = request.getContextPath();  
  4. String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";  
  5. %>  
  6. <!DOCTYPE html>  
  7. <html>  
  8.   <head>  
  9.     <title>通过Canvas及File API缩放并上传图片</title>  
  10.     <meta http-equiv="pragma" content="no-cache">  
  11.     <meta http-equiv="cache-control" content="no-cache">  
  12.     <meta http-equiv="expires" content="0">      
  13.     <meta http-equiv="keywords" content="Canvas,File,Image">  
  14.     <meta http-equiv="description" content="2013年8月8日,renfufei@qq.com">  
  15.       
  16.     <script src="http://code.jquery.com/jquery-1.7.1.min.js"></script>  
  17.     <script>  
  18.     // 参数,最大高度  
  19.     var MAX_HEIGHT = 100;  
  20.     // 渲染  
  21.     function render(src){  
  22.         // 创建一个 Image 对象  
  23.         var image = new Image();  
  24.         // 绑定 load 事件处理器,加载完成后执行  
  25.         image.onload = function(){  
  26.             // 获取 canvas DOM 对象  
  27.             var canvas = document.getElementById("myCanvas");  
  28.             // 如果高度超标  
  29.             if(image.height > MAX_HEIGHT) {  
  30.                 // 宽度等比例缩放 *=  
  31.                 image.width *= MAX_HEIGHT / image.height;  
  32.                 image.height = MAX_HEIGHT;  
  33.             }  
  34.             // 获取 canvas的 2d 环境对象,  
  35.             // 可以理解Context是管理员,canvas是房子  
  36.             var ctx = canvas.getContext("2d");  
  37.             // canvas清屏  
  38.             ctx.clearRect(0, 0, canvas.width, canvas.height);  
  39.             // 重置canvas宽高  
  40.             canvas.width = image.width;  
  41.             canvas.height = image.height;  
  42.             // 将图像绘制到canvas上  
  43.             ctx.drawImage(image, 0, 0, image.width, image.height);  
  44.             // !!! 注意,image 没有加入到 dom之中  
  45.         };  
  46.         // 设置src属性,浏览器会自动加载。  
  47.         // 记住必须先绑定事件,才能设置src属性,否则会出同步问题。  
  48.         image.src = src;  
  49.     };  
  50.     // 加载 图像文件(url路径)  
  51.     function loadImage(src){  
  52.         // 过滤掉 非 image 类型的文件  
  53.         if(!src.type.match(/image.*/)){  
  54.             if(window.console){  
  55.                 console.log("选择的文件类型不是图片: ", src.type);  
  56.             } else {  
  57.                 window.confirm("只能选择图片文件");  
  58.             }  
  59.             return;  
  60.         }  
  61.   
  62.         // 创建 FileReader 对象 并调用 render 函数来完成渲染.  
  63.         var reader = new FileReader();  
  64.         // 绑定load事件自动回调函数  
  65.         reader.onload = function(e){  
  66.             // 调用前面的 render 函数  
  67.             render(e.target.result);  
  68.         };  
  69.         // 读取文件内容  
  70.         reader.readAsDataURL(src);  
  71.     };  
  72.   
  73.     // 上传图片,jQuery版  
  74.     function sendImage(){  
  75.       // 获取 canvas DOM 对象  
  76.       var canvas = document.getElementById("myCanvas");  
  77.       // 获取Base64编码后的图像数据,格式是字符串  
  78.       // "data:image/png;base64,"开头,需要在客户端或者服务器端将其去掉,后面的部分可以直接写入文件。  
  79.       var dataurl = canvas.toDataURL("image/png");  
  80.       // 为安全 对URI进行编码  
  81.       // data%3Aimage%2Fpng%3Bbase64%2C 开头  
  82.       var imagedata =  encodeURIComponent(dataurl);  
  83.       //var url = $("#form").attr("action");  
  84.       // 1. 如果form表单不好处理,可以使用某个hidden隐藏域来设置请求地址  
  85.       // <input type="hidden" name="action" value="receive.jsp" />  
  86.       var url = $("input[name='action']").val();  
  87.       // 2. 也可以直接用某个dom对象的属性来获取  
  88.       // <input id="imageaction" type="hidden" action="receive.jsp">  
  89.       // var url = $("#imageaction").attr("action");  
  90.         
  91.       // 因为是string,所以服务器需要对数据进行转码,写文件操作等。  
  92.       // 个人约定,所有http参数名字全部小写  
  93.       console.log(dataurl);  
  94.       //console.log(imagedata);  
  95.       var data = {  
  96.                 imagename: "myImage.png",  
  97.                 imagedata: imagedata  
  98.             };  
  99.       jQuery.ajax( {  
  100.         url : url,  
  101.         data : data,  
  102.         type : "POST",  
  103.         // 期待的返回值类型  
  104.         dataType: "json",  
  105.         complete : function(xhr,result) {  
  106.             //console.log(xhr.responseText);  
  107.             var $tip2 = $("#tip2");  
  108.             if(!xhr){  
  109.                 $tip2.text('网络连接失败!');  
  110.                 return false;  
  111.             }  
  112.               
  113.             var text = xhr.responseText;  
  114.             if(!text){  
  115.                 $tip2.text('网络错误!');  
  116.                 return false;  
  117.             }  
  118.               
  119.             var json = eval("("+text+")");  
  120.             if(!json){  
  121.                 $tip2.text('解析错误!');  
  122.                 return false;  
  123.             } else {  
  124.                 $tip2.text(json.message);  
  125.             }  
  126.             //console.dir(json);  
  127.             //console.log(xhr.responseText);  
  128.         }  
  129.       });  
  130.     };  
  131.     function init(){  
  132.         // 获取DOM元素对象  
  133.         var target = document.getElementById("drop-target");  
  134.         // 阻止 dragover(拖到DOM元素上方) 事件传递  
  135.         target.addEventListener("dragover", function(e){e.preventDefault();}, true);  
  136.         // 拖动并放开鼠标的事件  
  137.         target.addEventListener("drop", function(e){  
  138.             // 阻止默认事件,以及事件传播  
  139.             e.preventDefault();   
  140.             // 调用前面的加载图像 函数,参数为dataTransfer对象的第一个文件  
  141.             loadImage(e.dataTransfer.files[0]);  
  142.         }, true);  
  143.         var setheight = document.getElementById("setheight");  
  144.         var maxheight = document.getElementById("maxheight");  
  145.         setheight.addEventListener("click", function(e){  
  146.                 //  
  147.                 var value = maxheight.value;  
  148.                 if(/^\d+$/.test(value)){  
  149.                     MAX_HEIGHT = parseInt(value);  
  150.                 }  
  151.                 e.preventDefault();  
  152.             },true);  
  153.         var btnsend = document.getElementById("btnsend");  
  154.         btnsend.addEventListener("click", function(e){  
  155.             //  
  156.             sendImage();  
  157.         },true);  
  158.     };  
  159.     window.addEventListener("DOMContentLoaded", function() {  
  160.         //  
  161.         init();  
  162.     },false);  
  163.     </script>  
  164.   </head>  
  165.     
  166.   <body>  
  167.    <div>  
  168.     <h1>通过Canvas及File API缩放并上传图片</h1>  
  169.     <p>从文件夹拖动一张照片到下方的盒子里, canvas 和 JavaScript将会自动的进行缩放.</p>  
  170.     <div>  
  171.         <input type="text" id="maxheight" value="100"/>   
  172.         <button id="setheight">设置图片最大高度</button>  
  173.         <input type="hidden" name="action" value="receive.jsp" />  
  174.     </div>  
  175.     <div id="preview-row">  
  176.         <div id="drop-target" style="width:400px;height:200px;min-height:100px;min-width:200px;background:#eee;cursor:pointer;">拖动图片文件到这里...</div>  
  177.         <div>  
  178.             <div>  
  179.                 <button id="btnsend"> 上 传 </button> <span id="tip2" style="padding:8px 0;color:#f00;"></span>  
  180.             </div>  
  181.         </div>  
  182.         <div><h4>缩略图:</h4></div>  
  183.         <div id="preview" style="background:#f4f4f4;width:400px;height:200px;min-height:100px;min-width:200px;">  
  184.             <canvas id="myCanvas"></canvas>  
  185.         </div>  
  186.     </div>  
  187.    </div>  
  188.   </body>  
  189. </html>  

服务端页面,receive.jsp

[html]  view plain copy
  1. <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>  
  2. <%@page import="sun.misc.BASE64Decoder"%>  
  3. <%@page import="java.io.*"%>  
  4. <%@page import="org.springframework.web.util.UriComponents"%>  
  5. <%@page import="java.net.URLDecoder"%>  
  6. <%!  
  7.     // 本文件:/receive.jsp  
  8.     // 图片存放路径  
  9.     String photoPath = "D:/blog/upload/photo/";  
  10.     File photoPathFile = new File(photoPath);  
  11.     // references: http://blog.csdn.net/remote_roamer/article/details/2979822  
  12.       
  13.     private boolean saveImageToDisk(byte[] data,String imageName) throws IOException{  
  14.         int len = data.length;  
  15.         //  
  16.         // 写入到文件  
  17.         FileOutputStream outputStream = new FileOutputStream(new File(photoPathFile,imageName));  
  18.           
  19.         outputStream.write(data);  
  20.         outputStream.flush();  
  21.         outputStream.close();  
  22.         //  
  23.         return true;  
  24.     }  
  25.     private byte[] decode(String imageData) throws IOException{  
  26.         BASE64Decoder decoder = new BASE64Decoder();  
  27.         byte[] data = decoder.decodeBuffer(imageData);  
  28.         for(int i=0;i<data.length;++i)  
  29.         {  
  30.             if(data[i]<0)  
  31.             {  
  32.                 //调整异常数据  
  33.                 data[i]+=256;  
  34.             }  
  35.         }  
  36.         //  
  37.         return data;  
  38.     }  
  39. %>  
  40. <%  
  41. String path = request.getContextPath();  
  42. String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";  
  43. %>  
  44. <%  
  45. //如果是IE,那么需要设置为text/html,否则会弹框下载  
  46. //response.setContentType("text/html;charset=UTF-8");  
  47. response.setContentType("application/json;charset=UTF-8");  
  48. //  
  49. String imageName = request.getParameter("imagename");  
  50. String imageData = request.getParameter("imagedata");  
  51. int success = 0;  
  52. String message = "";  
  53. if(null == imageData || imageData.length() < 100){  
  54.     // 数据太短,明显不合理  
  55.     message = "上传失败,数据太短或不存在";  
  56. } else {  
  57.     // 去除开头不合理的数据  
  58.     imageData = imageData.substring(30);  
  59.     imageData = URLDecoder.decode(imageData,"UTF-8");  
  60.       
  61.     //System.out.println(imageData);  
  62.     byte[] data = decode(imageData);  
  63.       
  64.     int len = data.length;  
  65.     int len2 = imageData.length();  
  66.     if(null == imageName || imageName.length() < 1){  
  67.         imageName = System.currentTimeMillis()+".png";  
  68.     }  
  69.     saveImageToDisk(data,imageName);  
  70.     //  
  71.     success = 1;  
  72.     message = "上传成功,参数长度:"+len2+"字符,解析文件大小:"+len+"字节";  
  73. }  
  74. // 后台打印  
  75. System.out.println("message="+message);  
  76.   
  77. %>  
  78. {  
  79.   "message": "<%=message %>",  
  80.   "success": <%=success %>  
  81. }  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值