欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

通过Canvas及File API缩放并上传图片完整示例

程序员文章站 2023-12-09 15:08:45
创建一个只管的用户界面,并允许你控制图片的大小。上传到服务器端的数据,并不需要处理enctype为 multi-part/form-data 的情况,仅仅一个简单的POST表单处理程序就可以了. 好了,下... 13-08-08...
示例地址:canvas resize demo
原文作者:dr. tom trenka
原文日期: 2013年8月6日
翻译日期: 2013年8月8日

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像素,不管原始图像有多高。基本的代码如下所示:

复制代码
代码如下:

// 参数,最大高度
var max_height = 100;
// 渲染
function render(src){
// 创建一个 image 对象
var image = new image();
// 绑定 load 事件处理器,加载完成后执行
image.onload = function(){
// 获取 canvas dom 对象
var canvas = document.getelementbyid("mycanvas");
// 如果高度超标
if(image.height > max_height) {
// 宽度等比例缩放 *=
image.width *= max_height / image.height;
image.height = max_height;
}
// 获取 canvas的 2d 环境对象,
// 可以理解context是管理员,canvas是房子
var ctx = canvas.getcontext("2d");
// canvas清屏
ctx.clearrect(0, 0, canvas.width, canvas.height);
// 重置canvas宽高
canvas.width = image.width;
canvas.height = image.height;
// 将图像绘制到canvas上
ctx.drawimage(image, 0, 0, image.width, image.height);
// !!! 注意,image 没有加入到 dom之中
};
// 设置src属性,浏览器会自动加载。
// 记住必须先绑定事件,才能设置src属性,否则会出同步问题。
image.src = src;
};

在上面的例子中,你可以使用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来加载和渲染图像就变得很容易了:

复制代码
代码如下:

// 加载 图像文件(url路径)
function loadimage(src){
// 过滤掉 非 image 类型的文件
if(!src.type.match(/image.*/)){
if(window.console){
console.log("选择的文件类型不是图片: ", src.type);
} else {
window.confirm("只能选择图片文件");
}
return;
}
// 创建 filereader 对象 并调用 render 函数来完成渲染.
var reader = new filereader();
// 绑定load事件自动回调函数
reader.onload = function(e){
// 调用前面的 render 函数
render(e.target.result);
};
// 读取文件内容
reader.readasdataurl(src);
};

请问,如何获取文件呢?
小白兔,要有耐心!我们的下一步就是获取文件,当然有好多方法可以实现啦。例如:你可以用文本框让用户输入文件路径,但很显然大多数用户都不是开发者,对输入什么值根本就不了解.
为了用户使用方便,我们采用 drag and drop api接口。

使用 drag and drop api
拖拽接口(drag and drop)非常简单——在大多数的dom元素上,你都可以通过绑定事件处理器来实现. 只要用户从磁盘上拖动一个文件到dom对象上并放开鼠标,那我们就可以读取这个文件。代码如下:

复制代码
代码如下:

function init(){
// 获取dom元素对象
var target = document.getelementbyid("drop-target");
// 阻止 dragover(拖到dom元素上方) 事件传递
target.addeventlistener("dragover", function(e){e.preventdefault();}, true);
// 拖动并放开鼠标的事件
target.addeventlistener("drop", function(e){
// 阻止默认事件,以及事件传播
e.preventdefault();
// 调用前面的加载图像 函数,参数为datatransfer对象的第一个文件
loadimage(e.datatransfer.files[0]);
}, true);
var setheight = document.getelementbyid("setheight");
var maxheight = document.getelementbyid("maxheight");
setheight.addeventlistener("click", function(e){
//
var value = maxheight.value;
if(/^\d+$/.test(value)){
max_height = parseint(value);
}
e.preventdefault();
},true);
var btnsend = document.getelementbyid("btnsend");
btnsend.addeventlistener("click", function(e){
//
sendimage();
},true);
};

我们还可以做一些其他的处理,比如显示预览图。但如果不想压缩图片的话,那很可能没什么用。我们将采用ajax通过http 的post方式上传图片数据。下面的例子是使用dojo框架来完成请求的,当然你也可以采用其他的ajax技术来实现.
dojo 代码如下:

复制代码
代码如下:

// 译者并不懂dojo,所以将在后面附上jquery的实现
// remember that dtk 1.7+ is amd!
require(["dojo/request"], function(request){
// 设置请求url,参数,以及回调。
request.post("image-handler.php", {
data: {
imagename: "myimage.png",
imagedata: encodeuricomponent(document.getelementbyid("canvas").todataurl("image/png"))
}
}).then(function(text){
console.log("the server returned: ", text);
});
});

jquery 实现如下:

复制代码
代码如下:

// 上传图片,jquery版
function sendimage(){
// 获取 canvas dom 对象
var canvas = document.getelementbyid("mycanvas");
// 获取base64编码后的图像数据,格式是字符串
// "data:image/png;base64,"开头,需要在客户端或者服务器端将其去掉,后面的部分可以直接写入文件。
var dataurl = canvas.todataurl("image/png");
// 为安全 对uri进行编码
// data%3aimage%2fpng%3bbase64%2c 开头
var imagedata = encodeuricomponent(dataurl);
//var url = $("#form").attr("action");
// 1. 如果form表单不好处理,可以使用某个hidden隐藏域来设置请求地址
// <input type="hidden" name="action" value="receive.jsp" />
var url = $("input[name='action']").val();
// 2. 也可以直接用某个dom对象的属性来获取
// <input id="imageaction" type="hidden" action="receive.jsp">
// var url = $("#imageaction").attr("action");
// 因为是string,所以服务器需要对数据进行转码,写文件操作等。
// 个人约定,所有http参数名字全部小写
console.log(dataurl);
//console.log(imagedata);
var data = {
imagename: "myimage.png",
imagedata: imagedata
};
jquery.ajax( {
url : url,
data : data,
type : "post",
// 期待的返回值类型
datatype: "json",
complete : function(xhr,result) {
//console.log(xhr.responsetext);
var $tip2 = $("#tip2");
if(!xhr){
$tip2.text('网络连接失败!');
return false;
}
var text = xhr.responsetext;
if(!text){
$tip2.text('网络错误!');
return false;
}
var json = eval("("+text+")");
if(!json){
$tip2.text('解析错误!');
return false;
} else {
$tip2.text(json.message);
}
//console.dir(json);
//console.log(xhr.responsetext);
}
});
};

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

复制代码
代码如下:

<%@ page language="java" import="java.util.*" pageencoding="utf-8"%>
<%
string path = request.getcontextpath();
string basepath = request.getscheme()+"://"+request.getservername()+":"+request.getserverport()+path+"/";
%>
<!doctype html>
<html>
<head>
<title>通过canvas及file api缩放并上传图片</title>
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
<meta http-equiv="keywords" content="canvas,file,image">
<meta http-equiv="description" content="2013年8月8日,renfufei@qq.com">
<script src="http://code.jquery.com/jquery-1.7.1.min.js"></script>
<script>
// 参数,最大高度
var max_height = 100;
// 渲染
function render(src){
// 创建一个 image 对象
var image = new image();
// 绑定 load 事件处理器,加载完成后执行
image.onload = function(){
// 获取 canvas dom 对象
var canvas = document.getelementbyid("mycanvas");
// 如果高度超标
if(image.height > max_height) {
// 宽度等比例缩放 *=
image.width *= max_height / image.height;
image.height = max_height;
}
// 获取 canvas的 2d 环境对象,
// 可以理解context是管理员,canvas是房子
var ctx = canvas.getcontext("2d");
// canvas清屏
ctx.clearrect(0, 0, canvas.width, canvas.height);
// 重置canvas宽高
canvas.width = image.width;
canvas.height = image.height;
// 将图像绘制到canvas上
ctx.drawimage(image, 0, 0, image.width, image.height);
// !!! 注意,image 没有加入到 dom之中
};
// 设置src属性,浏览器会自动加载。
// 记住必须先绑定事件,才能设置src属性,否则会出同步问题。
image.src = src;
};
// 加载 图像文件(url路径)
function loadimage(src){
// 过滤掉 非 image 类型的文件
if(!src.type.match(/image.*/)){
if(window.console){
console.log("选择的文件类型不是图片: ", src.type);
} else {
window.confirm("只能选择图片文件");
}
return;
}
// 创建 filereader 对象 并调用 render 函数来完成渲染.
var reader = new filereader();
// 绑定load事件自动回调函数
reader.onload = function(e){
// 调用前面的 render 函数
render(e.target.result);
};
// 读取文件内容
reader.readasdataurl(src);
};
// 上传图片,jquery版
function sendimage(){
// 获取 canvas dom 对象
var canvas = document.getelementbyid("mycanvas");
// 获取base64编码后的图像数据,格式是字符串
// "data:image/png;base64,"开头,需要在客户端或者服务器端将其去掉,后面的部分可以直接写入文件。
var dataurl = canvas.todataurl("image/png");
// 为安全 对uri进行编码
// data%3aimage%2fpng%3bbase64%2c 开头
var imagedata = encodeuricomponent(dataurl);
//var url = $("#form").attr("action");
// 1. 如果form表单不好处理,可以使用某个hidden隐藏域来设置请求地址
// <input type="hidden" name="action" value="receive.jsp" />
var url = $("input[name='action']").val();
// 2. 也可以直接用某个dom对象的属性来获取
// <input id="imageaction" type="hidden" action="receive.jsp">
// var url = $("#imageaction").attr("action");
// 因为是string,所以服务器需要对数据进行转码,写文件操作等。
// 个人约定,所有http参数名字全部小写
console.log(dataurl);
//console.log(imagedata);
var data = {
imagename: "myimage.png",
imagedata: imagedata
};
jquery.ajax( {
url : url,
data : data,
type : "post",
// 期待的返回值类型
datatype: "json",
complete : function(xhr,result) {
//console.log(xhr.responsetext);
var $tip2 = $("#tip2");
if(!xhr){
$tip2.text('网络连接失败!');
return false;
}
var text = xhr.responsetext;
if(!text){
$tip2.text('网络错误!');
return false;
}
var json = eval("("+text+")");
if(!json){
$tip2.text('解析错误!');
return false;
} else {
$tip2.text(json.message);
}
//console.dir(json);
//console.log(xhr.responsetext);
}
});
};
function init(){
// 获取dom元素对象
var target = document.getelementbyid("drop-target");
// 阻止 dragover(拖到dom元素上方) 事件传递
target.addeventlistener("dragover", function(e){e.preventdefault();}, true);
// 拖动并放开鼠标的事件
target.addeventlistener("drop", function(e){
// 阻止默认事件,以及事件传播
e.preventdefault();
// 调用前面的加载图像 函数,参数为datatransfer对象的第一个文件
loadimage(e.datatransfer.files[0]);
}, true);
var setheight = document.getelementbyid("setheight");
var maxheight = document.getelementbyid("maxheight");
setheight.addeventlistener("click", function(e){
//
var value = maxheight.value;
if(/^\d+$/.test(value)){
max_height = parseint(value);
}
e.preventdefault();
},true);
var btnsend = document.getelementbyid("btnsend");
btnsend.addeventlistener("click", function(e){
//
sendimage();
},true);
};
window.addeventlistener("domcontentloaded", function() {
//
init();
},false);
</script>
</head>
<body>
<div>
<h1>通过canvas及file api缩放并上传图片</h1>
<p>从文件夹拖动一张照片到下方的盒子里, canvas 和 javascript将会自动的进行缩放.</p>
<div>
<input type="text" id="maxheight" value="100"/>
<button id="setheight">设置图片最大高度</button>
<input type="hidden" name="action" value="receive.jsp" />
</div>
<div id="preview-row">
<div id="drop-target" style="width:400px;height:200px;min-height:100px;min-width:200px;background:#eee;cursor:pointer;">拖动图片文件到这里...</div>
<div>
<div>
<button id="btnsend"> 上 传 </button> <span id="tip2" style="padding:8px 0;color:#f00;"></span>
</div>
</div>
<div><h4>缩略图:</h4></div>
<div id="preview" style="background:#f4f4f4;width:400px;height:200px;min-height:100px;min-width:200px;">
<canvas id="mycanvas"></canvas>
</div>
</div>
</div>
</body>
</html>

服务端页面,receive.jsp

复制代码
代码如下:

<%@ page language="java" import="java.util.*" pageencoding="utf-8"%>
<%@page import="sun.misc.base64decoder"%>
<%@page import="java.io.*"%>
<%@page import="org.springframework.web.util.uricomponents"%>
<%@page import="java.net.urldecoder"%>
<%!
// 本文件:/receive.jsp
// 图片存放路径
string photopath = "d:/blog/upload/photo/";
file photopathfile = new file(photopath);
// references: http://blog.csdn.net/remote_roamer/article/details/2979822
private boolean saveimagetodisk(byte[] data,string imagename) throws ioexception{
int len = data.length;
//
// 写入到文件
fileoutputstream outputstream = new fileoutputstream(new file(photopathfile,imagename));
outputstream.write(data);
outputstream.flush();
outputstream.close();
//
return true;
}
private byte[] decode(string imagedata) throws ioexception{
base64decoder decoder = new base64decoder();
byte[] data = decoder.decodebuffer(imagedata);
for(int i=0;i<data.length;++i)
{
if(data[i]<0)
{
//调整异常数据
data[i]+=256;
}
}
//
return data;
}
%>
<%
string path = request.getcontextpath();
string basepath = request.getscheme()+"://"+request.getservername()+":"+request.getserverport()+path+"/";
%>
<%
//如果是ie,那么需要设置为text/html,否则会弹框下载
//response.setcontenttype("text/html;charset=utf-8");
response.setcontenttype("application/json;charset=utf-8");
//
string imagename = request.getparameter("imagename");
string imagedata = request.getparameter("imagedata");
int success = 0;
string message = "";
if(null == imagedata || imagedata.length() < 100){
// 数据太短,明显不合理
message = "上传失败,数据太短或不存在";
} else {
// 去除开头不合理的数据
imagedata = imagedata.substring(30);
imagedata = urldecoder.decode(imagedata,"utf-8");
//system.out.println(imagedata);
byte[] data = decode(imagedata);
int len = data.length;
int len2 = imagedata.length();
if(null == imagename || imagename.length() < 1){
imagename = system.currenttimemillis()+".png";
}
saveimagetodisk(data,imagename);
//
success = 1;
message = "上传成功,参数长度:"+len2+"字符,解析文件大小:"+len+"字节";
}
// 后台打印
system.out.println("message="+message);
%>
{
"message": "<%=message %>",
"success": <%=success %>
}