项目需求:欲实现PC端图片从本地上传,在裁剪框中裁剪成一定比例的图片,该图片会在微信端同步展示,考虑手机的屏幕适配,需要宽度为640px(只需修改一下参数即可),本文以宽320px为例;
实现方式:点击+框,图片加载在canvas画布,前端按照320/110比例裁剪图片,裁剪后将base64编码传至后台,后台将base64编码字符串转换为图片,并返回图片保存信息的实体类至前端展示;
引入文件:<script src="lib/zm-canvas-crop.js"></script>
前端代码
html
<div class="form-group marginB15 no-padding">
<label class="col-xs-1 defined-control-label2">XX海报</label>
<div class="col-xs-10 marginT10">
<div class="text-font12 marginB5">上传本次XX标题海报</div>
<div class="crop-picker-wrap">
<div class="col-xs-5 no-left-padding direct-title-width">
<div class="direct-block2 overflow" id="direct-block2" >
<p style="margin-top: 48px;"><i class="glyphicon glyphicon-plus text-font18"></i></p>
<input type="file" id="ipt" value="" name="" class="crop-picker-file">
</div>
<div class="direct-title-img marginT10" style="display: none;">
<!--<img ng-src="{{USPAD_USER_DATA_PATH+livePosterFile.path+livePosterFile.fileName}}"/>-->
<img id="base64">
<div id="deleteDirectImg">
<div class="direct-circle2"></div>
<div class="direct-delete2">X</div>
</div>
</div>
</div>
</div>
<div class="col-xs-7 crop-wrapper" id="crop-wrapper" style="display: none;">
<div class="crop-area-wrapper">
<div class="canvas-box"></div>
</div>
<div class="crop-container marginT10">
<div class="col-xs-3 col-xs-push-2">
<button class="btn btn-primary btn-block" id="save" type="button">保存</button>
</div>
<div class="col-xs-3 col-xs-push-2">
<button class="btn btn-primary btn-block" id="cancel" type="button">取消</button>
</div>
</div>
</div>
</div>
</div>
js
1.controller中js为如下:
new ZmCanvasCrop({
path: $rootScope.USPAD_USER_DATA_PATH,
fileInput: $('#ipt')[0],
saveBtn: $('#save')[0],
cancelBtn:$('#cancel')[0],
deleteImg: $('#deleteDirectImg')[0],
box_width: 300, //剪裁容器的最大宽度
box_height: 200, //剪裁容器的最大高度
min_width: 320, //要剪裁图片的最小宽度
min_height: 110 //要剪裁图片的最小高度
},saveCallBack);
2.zm-canvas-crop.js
关于js的说明:①通过input type="file"上传文件,通过监控其change事件执行readFile方法,在此处进行基础判断,图片类型和大小等,然后将图片加载在画布中,进行裁剪;input[type='file']如果本次选择的文件和上次的文件相同,则change事件不生效,通过清空input的value值($('#ipt').val("");)解决。
②将从后台获取的图片信息id传至input的name,$('#ipt').attr("name",livePosterFile.id),在controller中获取该id,进行图片删除和保存提交或者整个页面编辑完成后取消后的图片删除功能;
var path;
var livePosterFile;
function ZmCanvasCrop(opt, saveCallBack){
path = opt.path;
this.init(opt);
this._option.crop_box_width = opt.box_width; //剪裁容器的最大宽度
this._option.crop_box_height = opt.box_height; //剪裁容器的最大高度
this._option.crop_min_width = opt.min_width; //要剪裁图片的最小宽度
this._option.crop_min_height = opt.min_height; //要剪裁图片的最小高度
this._option.crop_scale = opt.min_width / opt.min_height; //图片会按照最小宽高的比例裁剪
}
function saveCallBack(base64, imgName) {
//最终把此base64传给后端
$.ajax({
type:'POST',
async:false,
url: ctx+'/file/cutUpload',
data: {'base64': base64, 'mark': 'typhoon','imgName':imgName},
dataType:'json',
success: function (data) {
if(data.status=="00"){
var self = this;
livePosterFile = data.data;
//如果保存成功则图片显示
$('#base64').attr("src",path+livePosterFile.path+livePosterFile.fileName);
$('#ipt').attr("name",livePosterFile.id);
$('.direct-title-img').css('display','');
$('#crop-wrapper').css('display','none');
$('.canvas-box').css('display','none');
}else{
alert("图片上传失败");
}
}
});
}
ZmCanvasCrop.prototype = {
_$box : '',
_$canvasDown: '',
_$canvasUp: '',
_input : '',
_ctxUp: '',//裁剪区域canvas
_img : '',
_img_show: {
width: '',
height: '',
scale: '', //显示像素除以实际像素
crop_width: '',//要裁剪部分显示宽
crop_height: '',
min_width: '',//要裁剪部分显示最小宽度
min_height: ''
},
_option : {
crop_box_width: '', //图片操作区域宽限制
crop_box_height: '', //图片操作区域高限制
crop_min_width: '', //剪裁实际最小像素宽
crop_min_height: '', //剪裁实际最小像素高
crop_scale: '' //宽高比
},
_save: {
left: '',
top: '',
width: '',
height: ''
},
_resize_point: {
color: '#69f',
size: 8
},
_resize_btn: {},
init: function(opt){
var self = this;
self._input = opt.fileInput;
self._$box = $('.canvas-box'); //裁剪框
self._$titleImg = $('.direct-title-img'); //图片显示框
self._$block = $('#direct-block2'); //+框
self._wrapper = $('#crop-wrapper'); //整个裁剪框
self.readFile();
opt.saveBtn.addEventListener('click', function(){
self.save();
});
opt.deleteImg.addEventListener('click', function(){
self.deleteImg();
});
opt.cancelBtn.addEventListener('click', function () {
self._wrapper.css("display","none");
self._$block.css("display","");
$("#ipt").val("");
});
},
imgTrue: function(){
if(this._img.width < this._option.crop_min_width || this._img.height < this._option.crop_min_height){
return false;
}
return true;
},
readFile: function(){
var self = this;
if(typeof FileReader==='undefined'){
alert("抱歉,你的浏览器不支持 FileReader");
input.setAttribute('disabled','disabled');
}else{
this._input.addEventListener('change', readFile, false);
}
function readFile(){
var file = this.files[0];
if(!/image\/\jpg|png|bmp|jpeg/.test(file.type)){
alert("文件必须为图片!");
$('#ipt').val("");
return false;
}
if(file.size>1024*1024){
alert("图片大于1MB,请调整图片至小于等于1MB!");
$('#ipt').val("");
return false;
}
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function(e){
self.drawCavDown(this.result);
}
}
},
drawCavDown: function(src){
var self = this;
//清除上一次的
self._$box.html('');
self._save = {};
self._img_show = {};
self._img = new Image();
self._img.onload = function(){
if(!self.imgTrue()){
alert('图片大小必须大于:' + self._option.crop_min_width + '*' + self._option.crop_min_height);
$('#ipt').val("");
return;
}
self._$block.css('display','none');
self._wrapper.css('display','');
self._$box.css('display','');
//让宽或者高撑满
self.setShowImg();
self._img_show.scale = self._img_show.width / self._img.width;//缩放比例
//计算裁剪高亮区域的最小宽高
self._img_show.min_width = self._option.crop_min_width * self._img_show.scale;
self._img_show.min_height = self._option.crop_min_height * self._img_show.scale;
//初始化显示剪裁框宽高,按照宽或者高(更小)的一半显示,如果一半值小于最小可剪裁值,还是按最小剪裁值显示
var size;
if (self._img.width > self._img.height) {
size = self._img.height / 2;
if(size<self._option.crop_min_height){
self.resizeCrop({
width: self._option.crop_min_width,
height: self._option.crop_min_height
});
}else{
self.resizeCrop({
height: size,
width: size * self._option.crop_scale
});
}
} else {
size = self._img.width / 2;
if(size<self._option.crop_min_width){
self.resizeCrop({
width: self._option.crop_min_width,
height: self._option.crop_min_height
});
}else{
self.resizeCrop({
height: size / self._option.crop_scale ,
width: size
});
}
}
//绘制底层剪裁区域
drawDown();
//载入上层canvas
self.addUpCanvas();
//绑定松开鼠标事件
$(document).on('mouseup', function(){//在外部松开
$(document).off('mousemove');
});
}
self._img.src = src;
function drawDown(){
var $canvas = $('<canvas width="' + self._img_show.width + '" height="' + self._img_show.height + '"></canvas>');
self._$box.append($canvas);
var $ctx = $canvas[0].getContext('2d');
$ctx.drawImage(self._img, 0, 0, self._img_show.width, self._img_show.height);
//裁剪区域透明
$ctx.beginPath();
$ctx.fillStyle="rgba(0,0,0,0.6)";
$ctx.fillRect(0, 0, self._img_show.width, self._img_show.height);
self._$canvasDown = $canvas;
}
},
setResizePoint: function(direction, left, top){
return $('<div class="resize-point" style="width:' + this._resize_point.size +'px;height:' + this._resize_point.size + 'px;'+
'background: ' + this._resize_point.color + ';cursor:'+ direction +';position:absolute;'+
'left:'+ left +'px;top:'+ top +'px"></div>');
},
addUpCanvas: function(){
var self = this;
self.addResizeBtn();//添加放大缩小按钮
self._ctxUp = self._$canvasUp[0].getContext('2d');
self._ctxUp.drawImage(self._img, 0, 0, self._img_show.crop_width / self._img_show.scale, self._img_show.crop_height / self._img_show.scale,0, 0, self._img_show.crop_width, self._img_show.crop_height);
//初始化实际存储
self._save.left = 0;
self._save.top = 0;
self._save.width = self._img_show.crop_width / self._img_show.scale;
self._save.height = self._img_show.crop_height / self._img_show.scale;
self.upCanvasEvent();
},
//绑定鼠标按下事件
upCanvasEvent: function(){
var self = this;
self._$canvasUp.on('mousedown', cavMouseDown);
function cavMouseDown(e){
var canv = this;
//获取到按下时,鼠标和元素的相对位置,相对偏差
var relativeOffset = { x: e.clientX - $(canv).offset().left, y: e.clientY - $(canv).offset().top };
$(document).on('mousemove', function(e){
//阻止移动出图片区域
if(countPosition().left >= self._img_show.width - self._img_show.crop_width || countPosition().left <= 0) relativeOffset.x = e.clientX - $(canv).offset().left;
if(countPosition().top >= self._img_show.height - self._img_show.crop_height || countPosition().top<=0) relativeOffset.y = e.clientY - $(canv).offset().top;
$(canv).css({left: countPosition().left, top: countPosition().top });//移动上层canvas
//实际存储
self._save.left = countPosition().left / self._img_show.scale;
self._save.top = countPosition().top / self._img_show.scale;
self._save.width = self._img_show.crop_width / self._img_show.scale;
self._save.height = self._img_show.crop_height / self._img_show.scale;
//重绘剪裁区域
self._ctxUp.drawImage(self._img,
self._save.left, self._save.top, self._save.width, self._save.height,
0, 0, self._img_show.crop_width, self._img_show.crop_height
);
//设置缩放按钮位置
self.resizePosition();
function countPosition(){
var left = (e.clientX - relativeOffset.x) - self._$canvasDown.offset().left;//还要减去父元素到左边窗口的距离
var top = (e.clientY - relativeOffset.y) - self._$canvasDown.offset().top;//还要减去父元素到左边窗口的距离
return {left: left, top: top}
}
});
}
},
addResizeBtn: function(){
var self = this;
//载入方向按钮
var $seResize = self.setResizePoint('se-resize', self._img_show.crop_width - self._resize_point.size/2, self._img_show.crop_height - self._resize_point.size/2);
var $swResize = self.setResizePoint('sw-resize', -self._resize_point.size/2, self._img_show.crop_height - self._resize_point.size/2);
var $neResize = self.setResizePoint('ne-resize', self._img_show.crop_width - self._resize_point.size/2, -self._resize_point.size/2);
var $nwResize = self.setResizePoint('nw-resize', -self._resize_point.size/2, -self._resize_point.size/2);
var $canvas = $('<canvas class="overlay" width="' + self._img_show.crop_width + '" height="' + self._img_show.crop_height + '"></canvas>');
self._$box.append($canvas);
self._$canvasUp = $canvas;
self._$box.append($seResize);
self._$box.append($swResize);
self._$box.append($neResize);
self._$box.append($nwResize);
self._resize_btn.$se = $seResize;
self._resize_btn.$sw = $swResize;
self._resize_btn.$ne = $neResize;
self._resize_btn.$nw = $nwResize;
self.resizeEvent();
},
//绑定方向按钮事件
resizeEvent: function(){
var self = this;
$('.resize-point').on('mousedown', function(){
var pLeft = $(this).position().left + self._resize_point.size/2,
pTop = $(this).position().top + self._resize_point.size/2;
var upLeft = self._$canvasUp.position().left,
upTop = self._$canvasUp.position().top;
var noChangeX,noChangeY;
if(upLeft >= pLeft) noChangeX = -(upLeft + self._img_show.crop_width);//为负在右
else noChangeX = upLeft;
if(upTop >= pTop) noChangeY = -(upTop + self._img_show.crop_height);//为负在下
else noChangeY = upTop;
$(document).on('mousemove', function(e){
if(noChangeX >= 0 ){
self._$canvasUp.css("left", noChangeX)
}else{
self._$canvasUp.css("left", Math.abs(noChangeX) - self._img_show.crop_width);
}
if(noChangeY >= 0 ){
self._$canvasUp.css("top", noChangeY)
}else{
self._$canvasUp.css("top", Math.abs(noChangeY) - self._img_show.crop_height);
}
//阻止移动出图片区域
self._img_show.crop_width = Math.abs(Math.abs(noChangeX) - countPosition().left);
self._img_show.crop_height = self._img_show.crop_width / self._option.crop_scale;
if(noChangeX >= 0 && noChangeX + self._img_show.crop_width > self._img_show.width){
self._img_show.crop_width = self._img_show.width - noChangeX;
self._img_show.crop_height = self._img_show.crop_width / self._option.crop_scale;
}else if(noChangeX < 0 && Math.abs(noChangeX) - self._img_show.crop_width < 0 ){
self._img_show.crop_width = Math.abs(noChangeX);
self._img_show.crop_height = self._img_show.crop_width / self._option.crop_scale;
}
if(noChangeY >= 0 && noChangeY + self._img_show.crop_height > self._img_show.height) {
self._img_show.crop_height = self._img_show.height - noChangeY;
self._img_show.crop_width = self._img_show.crop_height * self._option.crop_scale;
}else if(noChangeY < 0 && Math.abs(noChangeY) - self._img_show.crop_height < 0){
self._img_show.crop_height = Math.abs(noChangeY);
self._img_show.crop_width = self._img_show.crop_height * self._option.crop_scale;
}
//如果宽高小于限制
if(self._img_show.crop_width < self._img_show.min_width){
self._img_show.crop_width = self._img_show.min_width;
self._img_show.crop_height = self._img_show.crop_width / self._option.crop_scale;
}
if(self._img_show.crop_height < self._img_show.min_height){
self._img_show.crop_height = self._img_show.min_height;
self._img_show.crop_width = self._img_show.crop_height / self._option.crop_scale;
}
//实际存储
if(noChangeX>=0){
self._save.left = noChangeX / self._img_show.scale;
}else{
self._save.left = (Math.abs(noChangeX) - self._img_show.crop_width) / self._img_show.scale;
}
if(noChangeY>=0){
self._save.top = noChangeY / self._img_show.scale;
}else{
self._save.top = (Math.abs(noChangeY) - self._img_show.crop_height) / self._img_show.scale;
}
self._save.width = self._img_show.crop_width / self._img_show.scale;
self._save.height = self._img_show.crop_height / self._img_show.scale;
//重绘剪裁区域,修改属性宽高,否则无效
self._$canvasUp.attr("width", self._img_show.crop_width);
self._$canvasUp.attr("height", self._img_show.crop_height);
self._ctxUp.drawImage(self._img,
self._save.left, self._save.top, self._save.width, self._save.height,
0, 0, self._img_show.crop_width, self._img_show.crop_height
);
self.resizePosition();
function countPosition(){//鼠标在底层canvas的相对位置
var left = e.clientX - self._$canvasDown.offset().left ;
var top = e.clientY - self._$canvasDown.offset().top ;
return {left: left, top: top}
}
});
});
},
resizePosition: function(){
var self = this;
self._resize_btn.$se.css({left: self._$canvasUp.position().left + self._img_show.crop_width- self._resize_point.size/2, top: self._$canvasUp.position().top + self._img_show.crop_height - self._resize_point.size/2});//加上宽高,减去本身大小
self._resize_btn.$sw.css({left: self._$canvasUp.position().left - self._resize_point.size/2, top: self._$canvasUp.position().top + self._img_show.crop_height - self._resize_point.size/2});//加上宽高,减去本身大小
self._resize_btn.$ne.css({left: self._$canvasUp.position().left + self._img_show.crop_width - self._resize_point.size/2, top: self._$canvasUp.position().top - self._resize_point.size/2});//加上宽高,减去本身大小
self._resize_btn.$nw.css({left: self._$canvasUp.position().left - self._resize_point.size/2, top: self._$canvasUp.position().top - self._resize_point.size/2});//加上宽高,减去本身大小
},
parseInt: function(){
this._save.width = parseInt(this._save.width);
this._save.height = parseInt(this._save.height);
this._save.top = parseInt(this._save.top);
this._save.left = parseInt(this._save.left);
},
//保存
save: function(){
this.parseInt();//取整,避免出现杂边线条
var self = this;
var $result = $("<canvas width='"+ self._save.width +"' height='"+ self._save.height +"'></canvas>");
// $('body').append($result);
$result[0].getContext('2d').drawImage(self._img,
self._save.left, self._save.top, self._save.width, self._save.height,
0, 0, self._save.width, self._save.height
);
var base64Url = $result[0].toDataURL('image/jpeg');
saveCallBack && saveCallBack(base64Url,self._input.files[0].name);
return base64Url;
},
//删除图片
deleteImg: function(){
if(checkNotNull(livePosterFile)){
$.ajax({
type:'GET',
async:false,
url: ctx+'/file/deleteFile',
data: {'id': livePosterFile.id},
dataType:'json',
success: function (data) {
if(data.status=="00"){
livePosterFile = "";
$('#base64').attr("src","");
$('.direct-title-img').css('display','none');
$('#direct-block2').css('display','');
$('#ipt').attr("name","");
$('#ipt').val('');
}else{
alert("图片删除失败");
}
}
});
}
},
//显示的图片大小,三种结果,撑满宽或者高,或者原图大小
setShowImg: function(){
// console.log(this+";"+this._img.width+";"+this._option.crop_box_width+";"+this._img.height";"+";");
if( this._img.width <= this._option.crop_box_width && this._img.height <= this._option.crop_box_height ) {
this._img_show.width = this._img.width;
this._img_show.height = this._img.height;
return;
}
var weight = 0;//设置权重
if( this._img.width > this._option.crop_box_width ) weight+=10;
if( this._img.height > this._option.crop_box_height ) weight-=10;
if( this._img.width / this._img.height > this._option.crop_box_width / this._option.crop_box_height) weight+=5;
else weight-=5;
if( this._img.width >= this._img.height ) weight++;
else weight--;
if(weight > 0){//撑满宽度
this._img_show.width = this._option.crop_box_width;
this._img_show.height = this._option.crop_box_width / ( this._img.width / this._img.height );
}else{//撑满高度
this._img_show.height = this._option.crop_box_height;
this._img_show.width = this._option.crop_box_height / ( this._img.height / this._img.width );
}
},
resizeCrop: function(real){//剪裁框大小
this._img_show.crop_width = real.width * this._img_show.scale;
this._img_show.crop_height = real.height * this._img_show.scale;
}
}
css
扫描二维码关注公众号,回复:
4462704 查看本文章
.direct-title-width{
width: 349px;
}
.direct-delete2{
left: 321px;
}
.direct-circle2{
left: 316px;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #fff;
border: 1px solid #276399;
}
.direct-title-img{
position: relative;
padding: 2px 0;
height: 124px;
text-align: center;
border: 5px solid #D0CDC7;
color: #276399;
}
.direct-title-img img{
width: 320px;
height: 110px;
}
.direct-title-img div{
position: absolute;
top: -6px;
}
.direct-block2{
height: 122px;
text-align: center;
cursor: pointer;
border: 2px dashed #999;
}
.crop-picker-wrap {
position: relative;
/*width: 100px;*/
/*height: 30px;*/
overflow: hidden;
}
.crop-picker-file {
position: absolute;
top: 0;
left: 0px;
width: 330px;
height: 120px;
opacity: 0;
cursor: pointer;
filter: alpha(opacity=0);
}
.crop-wrapper {
display: inline-block;
margin: 10px 0;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
box-shadow: 0 0 5px 2px #ccc;
}
.crop-container {
font-size: 0;
}
.crop-area-wrapper {
width: 400px;
height: 200px;
}
.canvas-box{
width: 300px;
height: 200px;
position: relative;
-webkit-user-select:none;
-moz-user-select:none;
-o-user-select:none;
user-select:none
}
.canvas-box .overlay{
position: absolute;
left: 0;
top: 0;
cursor: move;
border:1px solid #69f;
}
.canvas-box div,.canvas-box canvas{
-webkit-user-select:none;
-moz-user-select:none;
-o-user-select:none;
user-select:none
}
后台代码
controller
@RestController
@RequestMapping("file")
public class UspadFileController {
@Autowired
private UspadFileService uspadFileService;
/**删除文件信息*/
@GetMapping("deleteFile")
public Map<String,Object> deleteFile(@RequestParam(name="id",required=true)String[] ids){
if(ids!=null && ids.length>0){
for(String id : ids){
uspadFileService.deleteFile(id);
}
}
return ResultMapHelper.success();
}
/**裁剪图片保存*/
@PostMapping("cutUpload")
public Map<String, Object> cutUpload(@RequestParam(name="base64",required=true) String base64,
@RequestParam(name="mark",required=true) String mark,
@RequestParam(name="imgName",required=true) String imgName) throws Exception {
if (base64 == null || StringUtils.isBlank(base64) || StringUtils.isBlank(mark) || StringUtils.isBlank(imgName)) {
return ResultMapHelper.nullParamsResult();
}
UspadFile ufile = uspadFileService.cutUploadFile(base64, mark, imgName);
return ResultMapHelper.success(ufile);
}
}
service
/**删除图片信息*/
@Transactional
public Map<String, Object> deleteFile(String id) {
UspadFile file = uspadFileDao.findOne(id);
if(file!=null){
uspadFileDao.delete(file);
return ResultMapHelper.success();
}else{
return ResultMapHelper.failure("未找到此文件");
}
}
/**裁剪图片保存*/
@Transactional
public UspadFile cutUploadFile(String base64, String mark, String imgName) throws IOException {
base64 = base64.substring(23);
String originalName = imgName;
String extension = imgName.substring(imgName.indexOf(".")+1, imgName.length());
String path = "/" + mark+"/"+
DateTimeUtils.converDateToString(new Date(), DateTimeUtils.DATE_PATTERN_DAY_01)+"/";
File folder = new File(AppConfig.getProperty("user_data_path")+path);
if(!folder.exists()){
folder.mkdirs();
}
String savePath = AppConfig.getProperty("user_data_path")+path+originalName;//图片终极路径
Img2Base64Util.generateImage(base64,savePath);//将base64编码字符串转换为图片
UspadFile uFile = new UspadFile();
uFile.setCreateTime(DateTimeUtils.getCurrentTime());
uFile.setFileName(originalName);
uFile.setFileType(extension);
uFile.setHost1(AppConfig.getProperty("server_ip"));
uFile.setMark(mark);
uFile.setOriginName(originalName);
uFile.setPath(path);
uspadFileDao.save(uFile);
//保存以后,拿到ID,如果是台风图片则传送图片
if(Constants.MARK_TYPHOON.equals(mark)){
String result = wxUrlService.sendFile(savePath, uFile.getId());
if(StringUtils.isNotBlank(result)){
//保存推送状态
uFile.setSendResult(result);
uspadFileDao.save(uFile);//再保存一次
}
}
return uFile;
}
实现效果
注:此为个人笔记及总结,有误或有更好的解决方案请指正!谢谢