前端图片压缩与水印

需求与原理

  1. 实现前端图片压缩
  2. 实现图片水印
    通过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 });
}