HTML5 本地裁剪图片并上传至服务器

时间:2022-09-18 22:40:27

转自:http://segmentfault.com/a/1190000000754560

很多情况下用户上传的图片都需要经过裁剪,比如头像啊什么的。但以前实现这类需求都很复杂,往往需要先把图片上传到服务器,然后返回给用户,让用户确定裁剪坐标,发送给服务器,服务器裁剪完再返回给用户,来回需要 5 步。步骤繁琐不说,当很多用户上传图片的时候也很影响服务器性能。

HTML5 的出现让我们可以更方便的实现这一需求。虽然这里所说的技术都貌似有点过时了(前端界的“过时”,你懂的),但还是有些许参考价值。在这里我只说一下要点,具体实现同学们慢慢研究。

下面奉上我自己写的一个demo,在输入框中选好自己服务器 url, 生成好图片后点击 Submit 上传,然后自己去服务器里看看效果吧~~

浏览器要求支持以下 Feature:

代码直接从现有项目移植过来,没有经过“太多的”测试,写的很乱,也没注释,大家就慢慢看吧。。。重点就在 js 脚本的 28 行,clipImage 函数中,同学们可以直接跳过去看。

http://jsfiddle.net/windwhinny/d5qan0q7/

JS

var tmp=$('<div class="resizer">'+
'<div class="inner">'+
'<img>'+
'<div class="frames"></div>'+
'</div>'+
//'<button>✗</button>'+
'<button class="ok">✓</button>'+
'</div>');
$.imageResizer=function(){
if(Uint8Array&&HTMLCanvasElement&&atob&&Blob){

}else{
return false;
}
var resizer=tmp.clone();

resizer.image=resizer.find('img')[0];
resizer.frames=resizer.find('.frames');
resizer.okButton=resizer.find('button.ok');
resizer.frames.offset={
top:0,
left:0
};

resizer.okButton.click(function(){
resizer.clipImage();
});
resizer.clipImage=function(){
var nh=this.image.naturalHeight,
nw=this.image.naturalWidth,
size=nw>nh?nh:nw;

size=size>1000?1000:size;

var canvas=$('<canvas width="'+size+'" height="'+size+'"></canvas>')[0],
ctx=canvas.getContext('2d'),
scale=nw/this.offset.width,
x=this.frames.offset.left*scale,
y=this.frames.offset.top*scale,
w=this.frames.offset.size*scale,
h=this.frames.offset.size*scale;

ctx.drawImage(this.image,x,y,w,h,0,0,size,size);
var src=canvas.toDataURL();
this.canvas=canvas;
this.append(canvas);
this.addClass('uploading');
this.removeClass('have-img');

src=src.split(',')[1];
if(!src)return this.doneCallback(null);
src=window.atob(src);

var ia = new Uint8Array(src.length);
for (var i = 0; i < src.length; i++) {
ia[i] = src.charCodeAt(i);
};

this.doneCallback(new Blob([ia], {type:"image/png"}));
};

resizer.resize=function(file,done){
this.reset();
this.doneCallback=done;
this.setFrameSize(0);
this.frames.css({
top:0,
left:0
});
var reader=new FileReader();
reader.onload=function(){
resizer.image.src=reader.result;
reader=null;
resizer.addClass('have-img');
resizer.setFrames();
};
reader.readAsDataURL(file);
};

resizer.reset=function(){
this.image.src='';
this.removeClass('have-img');
this.removeClass('uploading');
this.find('canvas').detach();
};

resizer.setFrameSize=function(size){
this.frames.offset.size=size;
return this.frames.css({
width:size+'px',
height:size+'px'
});
};

resizer.getDefaultSize=function(){
var width=this.find(".inner").width(),
height=this.find(".inner").height();
this.offset={
width:width,
height:height
};
console.log(this.offset)
return width>height?height:width;
};

resizer.moveFrames=function(offset){
var x=offset.x,
y=offset.y,
top=this.frames.offset.top,
left=this.frames.offset.left,
size=this.frames.offset.size,
width=this.offset.width,
height=this.offset.height;

if(x+size+left>width){
x=width-size;
}else{
x=x+left;
};

if(y+size+top>height){
y=height-size;
}else{
y=y+top;
};
x=x<0?0:x;
y=y<0?0:y;
this.frames.css({
top:y+'px',
left:x+'px'
});

this.frames.offset.top=y;
this.frames.offset.left=x;
};
(function(){
var time;
function setFrames(){
var size=resizer.getDefaultSize();
resizer.setFrameSize(size);
};

window.onresize=function(){
clearTimeout(time)
time=setTimeout(function(){
setFrames();
},1000);
};

resizer.setFrames=setFrames;
})();

(function(){
var lastPoint=null;
function getOffset(event){
event=event.originalEvent;
var x,y;
if(event.touches){
var touch=event.touches[0];
x=touch.clientX;
y=touch.clientY;
}else{
x=event.clientX;
y=event.clientY;
}

if(!lastPoint){
lastPoint={
x:x,
y:y
};
};

var offset={
x:x-lastPoint.x,
y:y-lastPoint.y
}
lastPoint={
x:x,
y:y
};
return offset;
};
resizer.frames.on('touchstart mousedown',function(event){
getOffset(event);
});
resizer.frames.on('touchmove mousemove',function(event){
if(!lastPoint)return;
var offset=getOffset(event);
resizer.moveFrames(offset);
});
resizer.frames.on('touchend mouseup',function(event){
lastPoint=null;
});
})();
return resizer;
};
var resizer=$.imageResizer(),
resizedImage;

if(!resizer){
resizer=$("<p>Your browser doesn't support these feature:</p><ul><li>canvas</li><li>Blob</li><li>Uint8Array</li><li>FormData</li><li>atob</li></ul>")
};

$('.container').append(resizer);

$('input').change(function(event){
var file=this.files[0];
resizer.resize(file,function(file){
resizedImage=file;
});
});

$('button.submit').click(function(){
var url=$('input.url').val();
if(!url||!resizedFile)return;
var fd=new FormData();
fd.append('file',resizedFile);
$.ajax({
type:'POST',
url:url,
data:fd
});
});

HTML

<input type="file" accept="images/*">
<input class="url" type="url" placeholder="url">
<div class="container"></div>
<button class="submit">Submit</button>

CSS

.container{
width: 300px;
}
.resizer{
overflow: hidden;
}
.resizer.have-img button.ok{
display: inline-block;
}
.resizer.have-img .inner {
display: block;
}
.inner{
width: 100%;
position: relative;
font-size: 0;
overflow: hidden;
display: none;
}
img{
width: 100%;
}

.frames{
position: absolute;
top: 0;
left: 0;
border: 1px solid black;
cursor: move;
outline: rgba(0, 0, 0, 0.6) solid 10000px;
}
button.ok{
float:right;
margin-left: 5px;
display: none;
}
canvas{
max-width: 100%;
margin:auto;
display: block;
}


第一步:获取文件

HTML5 支持从 input[type=file] 元素中直接获取文件信息,也可以读取文件内容。我们用下面代码就可以实现:

$('input[type=file]').change(function(){
var file=this.files[0];
// continue ...
});

第二部:读取文件,并生成 Image 元素

这一步就需要用到 FileReader 了,这个类是专门用来读取本地文件的。纯文本或者二进制都可以读取,但是本地文件必须是经过用户允许才能读取,也就是说用户要在input[type=file]中选择了这个文件,你才能读取到它。

通过 FileReader 我们可以将图片文件转化成 DataURL,就是以 data:image/png;base64, 开头的一种URL,然后可以直接放在 image.src 里,这样本地图片就显示出来了。

$('input[type=file]').change(function(){
var file=this.files[0];

var reader=new FileReader();
reader.onload=function(){
// 通过 reader.result 来访问生成的 DataURL
var url=reader.result;
setImageURL(url);
};
reader.readAsDataURL(file);
});

var image=new Image();
function setImageURL(url){
image.src=url;
}

Image 就是在 html 里的 <img> 标签,所以可以直接插入到文档流里。

第三步:获取裁剪坐标

这一步没啥好说的,实现的方法也很多,需要获得下面四个裁剪框的坐标:

  • Y坐标
  • X坐标
  • 高度
  • 宽度

如下图所示:
HTML5 本地裁剪图片并上传至服务器

第四部:裁剪图片

这是时候我们就需要用到 canvas 了,canvas 和图片一样,所以新建 canvas 时就要确定其高宽。这里我们还运用到image.naturalHeight 和 image.naturalWidth 这两个属性来获取图片原始尺寸。

将图片放置入 canvas 时需要调用 drawImage ,这个接口参数比较多,在 MDN 上有详细的说明。

drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

HTML5 本地裁剪图片并上传至服务器

因为我们用 canvas 只是用于裁剪图片的,所以需要新建一个 canvas 让它的尺寸和裁剪之后图片的尺寸相等,此时canvas 就相当与我们的裁剪框。运用这个函数还可以将大图缩放成小图,同学们自己研究吧。

// 以下四个参数由第三步获得
var x,
y,
width,
height;

var canvas=$('<canvas width="'+width+'" height="'+height+'"></canvas>')[0],
ctx=canvas.getContext('2d');

ctx.drawImage(image,x,y,width,height,0,0,width,height);
$(document.body).append(canvas);

将 canvas 加入文档流之后,就可以看到裁剪后的效果了。不过我们还需要将图片上传至服务器里。

第五步:读取裁剪后的图片并上传

这时我们要获取 canvas 中图片的信息,用 toDataURL 就可以转换成上面用到的 DataURL 。 然后取出其中 base64 信息,再用 window.atob 转换成由二进制字符串。但 window.atob 转换后的结果仍然是字符串,直接给 Blob 还是会出错。所以又要用 Uint8Array 转换一下。总之这里挺麻烦的。。

var data=canvas.toDataURL();

// dataURL 的格式为 “data:image/png;base64,****”,逗号之前都是一些说明性的文字,我们只需要逗号之后的就行了
data=data.split(',')[1];
data=window.atob(data);
var ia = new Uint8Array(data.length);
for (var i = 0; i < data.length; i++) {
ia[i] = data.charCodeAt(i);
};

// canvas.toDataURL 返回的默认格式就是 image/png
var blob=new Blob([ia], {type:"image/png"});

这时候裁剪后的文件就储存在 blob 里了,我们可以把它当作是普通文件一样,加入到 FormData 里,并上传至服务器了。

FormData 顾名思义,就是用来创建表单数据的,用 append 以键值的形式将数据加入进去即可。但他最大的特点就是可以手工添加文件或者 Blob 类型的数据,Blob 数据也会被当作文件来处理。原生 js 可以直接传递给 xhr.send(fd), jquery 可以放入 data 里请求。

var fd=new FormData();

fd.append('file',blob);
$.ajax({
url:"your.server.com",
type:"POST",
data:fd,
success:function(){}
});

然后你服务器里应该就可以收到这个文件了~