需求与原理
- 实现前端图片压缩
- 实现图片水印
通过canvas技术把图片资源绘到canvas画布上,把水印文字也绘到画布上,再通过canvas的toDateUrl方法导出base64或者blob格式的jpg或者png图片。
设计思路
设计模式上采用的是最常见的单体模式,封装一个imgTools的分发库。
一:首先把传入的图片(image图片元素,base64字符串,canvas对象,还有选择文件时候的file对象)统一转换成base64格式(包含
data:image/jpeg;base64,前缀部分),同时也取到了传入图片的实际大小,往后传,并执行配置中的初始图片加载完毕的回调方法。
二:根据配置中的压缩方式,计算图片的需要压缩的尺寸。
三:开始绘图,把传入的base64图片绘入canvas中。
四:水印中需要包含照片拍摄时间和照片的拍摄地点,使用exif.js获取照片的exif信息,取到GPS坐标(数组,需要转换成经纬度的形式),通过百度地图api反查详细地址。取到的拍摄时间格式是2018:12:08 12:12:00,需要转换一下,然后把水印信息绘入画布。
四:canvas.toDataURL把canvas转换成base64图片返回(jpg格式可选择压缩率),如果配置中选择返回blob,则转换成blob返回。
实现
1. 配置
{
resizeMode: 'auto', //压缩模式,总共有三种 auto,width,height auto表示自动根据最大的宽度及高度等比压缩,width表示只根据宽度来判断是否需要等比例压缩,height类似。
dataSource: null, //数据源。数据源是指需要压缩的数据源,有三种类型,image图片元素,base64字符串,canvas对象,还有选择文件时候的file对象。。。
dataSourceType: 'file', //image base64 canvas file
maxWidth: 960, //允许的最大宽度
maxHeight: 960, //允许的最大高度。
watermark: true, // 是否需要打水印
quality: 0.8, // jpg照片压缩比
zoom: true, // 是否要放大小图
content: [], // 水印文字
imageType: 'blob', // base64或者blob
location: {
lng: null,
lat: null
},
tempImgGenerate: function(img) {}, // 图片加载完毕的回调方法
success: function(resizeImgBase64, canvas) {}, // 图片导出完成回调方法
}
2. 包含的方法
读取file形式的图片,返回base64格式
getBase64FromImg: function(file, callBack) {
var reader = new FileReader();
reader.onload = function(e) {
var base64Img = e.target.result;
if (callBack) {
callBack(base64Img);
}
};
reader.readAsDataURL(file);
}
处理数据源 将所有数据源都处理成为图片对象
getImgFromDataSource: function(dataSource, dataSourceType, callback) {
let img = new Image();
new Promise((resolve, reject) => {
if (dataSourceType === 'img' || dataSourceType === 'image') {
img.src = dataSource.src;
resolve(img);
} else if (dataSourceType == 'base64') {
img.src = dataSource;
resolve(img);
} else if (dataSourceType == 'canvas') {
img.src = dataSource.toDataURL('image/jpeg');
resolve(img);
} else if (dataSourceType == 'file') {
this.getBase64FromImg(dataSource, function(base64str) {
img.src = base64str;
resolve(img);
});
}
}).then(img => {
if (callback) {
img.onload = img.onreadystatechange = () => {
callback(img);
};
}
});
}
计算图片的需要压缩的尺寸。当然,压缩模式,压缩限制直接从setting里面取出来。
getResizeSizeFromImg: function(img) {
let _img = {
w: img.width,
h: img.height,
scale: 1
};
if (_img.w <= conf.maxWidth && _img.h <= conf.maxHeight && !conf.zoom) {
_img.scale = parseFloat(_img.w / conf.maxWidth); // 图片拉伸比例,当图片尺寸偏小且不允许放大处理时,需要回传图片缩小比例给canvas绘图方法执行缩放
return _img;
}
if (conf.resizeMode === 'auto') {
let _scale = parseFloat(_img.w / _img.h);
let _size_by_mw = {
w: conf.maxWidth,
h: parseInt(conf.maxWidth / _scale),
scale: 1
};
let _size_by_mh = {
w: parseInt(conf.maxHeight * _scale),
h: conf.maxHeight,
scale: 1
};
if (_size_by_mw.h <= conf.maxHeight) {
return _size_by_mw;
}
if (_size_by_mh.w <= conf.maxWidth) {
return _size_by_mh;
}
return {
w: conf.maxWidth,
h: conf.maxHeight,
scale: 1
};
}
if (conf.resizeMode === 'width') {
if (_img.w <= conf.maxWidth) {
return _img;
}
var _size_by_mw = {
w: conf.maxWidth,
h: parseInt(conf.maxWidth / _scale)
};
return _size_by_mw;
}
if (conf.resizeMode === 'height') {
if (_img.h <= conf.maxHeight) {
return _img;
}
var _size_by_mh = {
w: parseInt(conf.maxHeight * _scale),
h: conf.maxHeight
};
return _size_by_mh;
}
}
exif.js获取照片拍摄信息
传入base64图片
EXIF.getData(img, function() {
let info = EXIF.getAllTags(this);
console.log(info);
})
百度地图接口反查详细地址信息
let lng = parseFloat((parseFloat(info.GPSLongitude[1]) + parseFloat(info.GPSLongitude[2] / 60)) / 60) + parseFloat(info.GPSLongitude[0]),
lat = parseFloat((parseFloat(info.GPSLatitude[1]) + parseFloat(info.GPSLatitude[2] / 60)) / 60) + parseFloat(info.GPSLatitude[0]);
let ggPoint = new BMap.Point(lng, lat),
geocoder = new BMap.Geocoder(),
convertor = new BMap.Convertor(), // 转换器
pointArr = [];
pointArr.push(ggPoint);
convertor.translate(pointArr, 1, 5, function(data) {
if (data.status === 0) {
geocoder.getLocation(data.points[0], function(_res) {
resolve('地点:' + _res.address);
});
} else {
reject();
}
});
canvas作图
需要注意的是,为了达到更好的文字适配效果,需要用到canvas的measureText方法动态计算文字的大小,并实现截字。
使用canvas.toDataURL(‘image/jpeg’, 0.8);导出图片,jpg格式可传入压缩比
let len3 = 0,
len4 = 0,
lineWidth1 = 0,
lineWidth2 = 0,
noSecondRow = true,
lineHeight;
str2 = res + '';
noSecondRow = !str2 && !str4;
lineHeight = (noSecondRow ? 40 : 80) * imgScale; // 没有地址和日期时,只显示一行
// 画矩形
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fillRect(0, theH - lineHeight, theW, lineHeight);
//画文字下
ctx.font = 'normal ' + 24 * imgScale + "px 'Helvetica Neue','Helvetica','PingFang SC','Hiragino Sans GB','Microsoft YaHei'";
ctx.textAlign = 'left';
ctx.textBaseline = 'bottom';
ctx.fillStyle = '#ffffff';
len3 = ctx.measureText(str3).width;
len4 = ctx.measureText(str4).width;
for (let i = 0; i < str1.length; i++) {
lineWidth1 += ctx.measureText(str1[i]).width;
if (lineWidth1 > canvas.width / ratio - len3 - 20 * imgScale) {
str1 = str1.substring(0, i - 3) + '...';
break;
}
}
for (let i = 0; i < str2.length; i++) {
lineWidth2 += ctx.measureText(str2[i]).width;
if (lineWidth2 > canvas.width / ratio - len4 - 20 * imgScale) {
str2 = str2.substring(0, i - 3) + '...';
break;
}
}
ctx.fillText(str1, 10 * imgScale, theH - (noSecondRow ? 8 : 40) * imgScale);
ctx.fillText(str2, 10 * imgScale, theH - 10 * imgScale);
ctx.textAlign = 'right';
ctx.fillText(str3, theW - 10 * imgScale, theH - (noSecondRow ? 8 : 40) * imgScale);
ctx.fillText(str4, theW - 10 * imgScale, theH - 10 * imgScale);
// 获取base64字符串及canvas对象传给success函数。
base64str = canvas.toDataURL('image/jpeg', 0.8);
if (callback) {
callback(base64str, canvas);
}
base64转blob
dataURLtoBlob: function(dataurl) {
var arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
}