canvas压缩、裁切图片和格式转换的方法

按照大小压缩图片,或者按照特定分辨率裁切图片,转为blob数据。自动处理ios中可能存在的照片偏差90°问题。

例如,获取300*300大小的头像,实现以下效果:

 使用方式:

<!-- 引入js文件 -->
    <script type="text/javascript" src="./compressImage.js"></script>
<!-- input标签 -->
    <input type="file" id="avatar" name="avatar" accept="image/png, image/jpeg">

如果想通过npn引入,请参考 git说明 。

裁取特定分辨率的图片(如300*300):

    compressImage({
        input: document.getElementById('avatar'),
        width: 300,
        height: 300,
        callback: function(blob, fileName) {
            // blob是处理之后的图片二进制数据
            // fileName是文件名,如"avatar.png"
            // ...
        }
    })

  

将图片压缩到指定大小,如500kb以下:

    compressImage({
        input: document.getElementById('avatar'),
        size: 500,
        callback: function(blob, fileName) {
            // ...
        }
    })

  

指定图片名字和格式(允许jpg和png格式互转):

    compressImage({
        input: document.getElementById('avatar'),
        size: 500,
        name: "user_avatar_1024687956"
        type: 'png',
        callback: function(blob, fileName) {
            // 此处得到的fileName就是 user_avatar_1024687956.png
            // ...
        }
    })

  

callback是图片处理之后的回调函数,图片会转为blob数据数据,blob的用法参考:

    callback: function(blob, fileName) {
        var url = URL.createObjectURL(blob);

        /**** 通过<img>显示 ****/
        var img = document.createElement("img");
        img.src = url;
        document.body.append(img);

        /**** formData上传图片 ****/
        var formData = new FormData();
        formData.append("file", blob, fileName);
        $.ajax({
            url: 'api/upload/img',
            type: 'POST',
            data: formData,
            success: function(returndata) {
                console.log("上传成功")
                formData = null;
            }
        })

        /**** 下载图片 ****/
        var a = document.createElement('a');
        a.setAttribute('download', fileName);
        a.href = url;
        a.click();
    }

  

compressImage方法:

/**
 * compressImage.js
 * 参数config:{ input, callback, name, type, quality, size, width, height}
 * input: 必填,input[type=file]的表单元素,支持multiple多张图片
 * callback: 必填,处理之后的回调函数,参数(blob,fileName)
 * name: 非必填,自定义文件名,不包含后缀(如.jpg),默认原文件名
 * type: 非必填,图片格式,可选png/jpg,默认原图片格式
 * quality: 非必填,图片质量系数,默认0.92
 * 只传size: 压缩图片至size(单位kb)大小
 * 传width: 根据宽度压缩图片,高度自适应
 * 传height: 根据高度压缩图片,宽度自适应
 * 传width和height: 压缩图片,从中心位置裁取
 * 不传size/width/height: 只进行格式转换,不压缩图片
 * 同时传size和width/height: 会忽略size,根据width/height处理
 **/

function compressImage(config) {
    if (!config.input || !config.input.files || config.input.files.length == 0) {
        console.log("compressImage: 无图片文件")
        return;
    }
    if (!config.callback) {
        console.log("compressImage: 缺少回调函数")
        return;
    }
    if (config.type && config.type != "png" && config.type != "jpg") {
        console.log("compressImage: 图片格式指定错误,请选择png或jpg")
        return;
    }
    config.quality = (config.quality && config.quality > 0 && config.quality <= 1) ? config.quality : 0.92;
    for (var i = 0; i < config.input.files.length; i++) {
        HANDLE_SINGLE_IMAGE(config.input.files[i], config)
    }
}

function HANDLE_SINGLE_IMAGE(file, config) {
    var idx = file.name.lastIndexOf(".");
    var imageName = file.name.substring(0, idx);
    var imageType = file.name.substring(idx + 1, file.name.length).toLowerCase();
    if (imageType != "png" && imageType != "jpg" && imageType != "jpeg") {
        console.log("compressImage: 不支持的图片格式 - " + imageType)
    } else {

        // fileType: canvas.toBlob方法的参数
        config.fileType = (!!config.type ? ("image/" + config.type.replace("jpg", "jpeg")) : file.type);

        // type: 文件名中的格式后缀
        config.type = config.type || imageType.replace("jpeg", "jpg");

        // fileName: 完整的文件名,将在callback中返回
        config.fileName = (config.name ? (config.name + "." + config.type) : (imageName + "." + config.type));

        // ios下的jpg文件需要修正照片方向
        var isIOS = (/iphone|ipad|mac/).test(window.navigator.userAgent.toLowerCase());
        if (isIOS && file.type == "image/jpeg") {
            var reader = new FileReader();
            reader.readAsArrayBuffer(file);
            reader.onload = function() {
                var orientation = GET_ORIENTATION(this.result);
                alert(orientation)
                IMAGE_READER(file, config, orientation)
            }
        } else {
            IMAGE_READER(file, config)
        }
    }
}

function IMAGE_READER(file, config, orientation) {
    var reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = function() {
        var img = document.createElement("img");
        img.src = this.result;
        img.onload = function() {
            if(orientation==6 || orientation==8){
                var origin_width = parseInt(this.width);
                this.width = parseInt(this.height);
                this.height = origin_width;
            }else{
                this.width = parseInt(this.width);
                this.height = parseInt(this.height);
            }

            // 标记是否只按照size要求去压缩
            var bySize = false;

            // 缩放后图片的尺寸,canvas将从中裁切
            var imgWidth = 0;
            var imgHeight = 0;

            // 目标尺寸,即最后生成的图片尺寸
            var targetWidth = 0;
            var targetHeight = 0;

            // config有width/height时
            if (config.width && config.height) {
                targetWidth = config.width;
                targetHeight = config.height;
                var ratio_x = this.width / targetWidth;
                var ratio_y = this.height / targetHeight;
                if (ratio_x > ratio_y) {
                    imgWidth = this.width / ratio_y;
                    imgHeight = targetHeight;
                } else {
                    imgWidth = targetWidth;
                    imgHeight = this.height / ratio_x;
                }
            }
            if (config.width && !config.height) {
                imgWidth = targetWidth = config.width;
                imgHeight = targetHeight = targetWidth / (this.width / this.height);
            }
            if (!config.width && config.height) {
                imgHeight = targetHeight = config.height;
                imgWidth = targetWidth = (this.width / this.height) * targetHeight;
            }
            if (targetWidth == 0 && targetHeight == 0) {
                // config有size时,根据大小进行压缩
                if (config.size && config.size > 0 && file.size > config.size * 1024) {
                    bySize = true;
                    var ratio = Math.sqrt((config.size * 1024) / file.size).toFixed(2);
                    if (ratio < 0.5) {
                        ratio = 0.5;
                    }
                    imgWidth = targetWidth = parseInt(this.width * ratio);
                    imgHeight = targetHeight = parseInt(this.height * ratio);
                } else {
                    // 不压缩或者裁切,只将图片转为blob数据
                    imgWidth = targetWidth = this.width;
                    imgHeight = targetHeight = this.height;
                }
            } else {
                targetWidth = parseInt(targetWidth);
                targetHeight = parseInt(targetHeight);
            }
            var canvas = document.createElement('canvas');
            var ctx = canvas.getContext('2d');
            canvas.width = targetWidth;
            canvas.height = targetHeight;
            
            // 矫正旋转方向
            switch (orientation) {
                case 3:
                    ctx.rotate(180 * Math.PI / 180);
                    ctx.drawImage(this, (imgWidth-targetWidth)/2-imgWidth, (imgHeight-targetHeight)/2-imgHeight, imgWidth, imgHeight );
                    break;
                case 6:
                    ctx.rotate(90 * Math.PI / 180);
                    ctx.drawImage(this, (targetHeight-imgHeight)/2, (imgWidth-targetWidth)/2-imgWidth, imgHeight , imgWidth);
                    break;
                case 8:
                    ctx.rotate(270 * Math.PI / 180);
                    ctx.drawImage(this, (imgHeight-targetHeight)/2-imgHeight, (targetWidth-imgWidth)/2, imgHeight , imgWidth);
                    break;
                default:
                    ctx.drawImage(this, (targetWidth-imgWidth)/2, (targetHeight-imgHeight)/2, imgWidth, imgHeight);
            }

            canvas.toBlob(function(blob) {
                if (bySize && blob.size >= config.size * 1024) {
                    COMPRESS_BY_SIZE(blob, config, canvas, ctx)
                    return;
                }
                config.callback(blob, config.fileName)
            }, config.fileType, config.quality);
        }
    }
}

//将图片按0.9倍缩小至目标size
function COMPRESS_BY_SIZE(old_blob, config, canvas, ctx) {
    console.log("COMPRESS_BY_SIZE")
    config.quality = 0.98;
    var reader = new FileReader();
    reader.readAsDataURL(old_blob);
    reader.onload = function() {
        var img = document.createElement("img");
        img.src = this.result;
        img.onload = function() {
            width = parseInt(img.width * 0.9);
            height = parseInt(img.height * 0.9);
            canvas.width = width;
            canvas.height = height;
            ctx.drawImage(img, 0, 0, width, height);
            canvas.toBlob(function(blob) {
                if (blob.size >= config.size * 1024) {
                    COMPRESS_BY_SIZE(blob, config, canvas, ctx);
                    return;
                }
                config.callback(blob, config.fileName)
            }, config.fileType, config.quality);
        }
    }
}

/**
 * 获取iOS照片的旋转角度
 * 1-0° 3-180° 6-90° 8-270°
 **/
function GET_ORIENTATION(arrayBuffer) {
    var dataView = new DataView(arrayBuffer);
    var length = dataView.byteLength;
    var orientation = 0;
    var exifIDCode;
    var tiffOffset;
    var firstIFDOffset;
    var littleEndian;
    var endianness;
    var app1Start;
    var ifdStart;
    var offset;
    var i;
    if (dataView.getUint8(0) === 0xFF && dataView.getUint8(1) === 0xD8) {
        offset = 2;
        while (offset < length) {
            if (dataView.getUint8(offset) === 0xFF && dataView.getUint8(offset + 1) === 0xE1) {
                app1Start = offset;
                break;
            }
            offset++;
        }
    }
    if (app1Start) {
        exifIDCode = app1Start + 4;
        tiffOffset = app1Start + 10;
        if (GET_CHARCODE_STRING(dataView, exifIDCode, 4) === 'Exif') {
            endianness = dataView.getUint16(tiffOffset);
            littleEndian = endianness === 0x4949;
            if (littleEndian || endianness === 0x4D4D) {
                if (dataView.getUint16(tiffOffset + 2, littleEndian) === 0x002A) {
                    firstIFDOffset = dataView.getUint32(tiffOffset + 4, littleEndian);
                    if (firstIFDOffset >= 0x00000008) {
                        ifdStart = tiffOffset + firstIFDOffset;
                    }
                }
            }
        }
    }
    if (ifdStart) {
        length = dataView.getUint16(ifdStart, littleEndian);
        for (i = 0; i < length; i++) {
            offset = ifdStart + i * 12 + 2;
            if (dataView.getUint16(offset, littleEndian) === 0x0112) {
                offset += 8;
                orientation = dataView.getUint16(offset, littleEndian);
                break;
            }
        }
    }
    return orientation;
}

function GET_CHARCODE_STRING(dataView, start, length) {
    var str = '';
    var i;
    for (i = start, length += start; i < length; i++) {
        str += String.fromCharCode(dataView.getUint8(i));
    }
    return str;
}

 

posted @ 2020-04-15 12:59  前端大兵  阅读(755)  评论(0编辑  收藏  举报