前端图片压缩并保留EXIF信息

乱弹:

前段时间在写公司的上传组件,有个需求是前端压缩图片,并且保留EXIF信息,然后上传到服务器,IE6-9依赖的是库 mOxie,具体的就戳前面的地址吧,百度肯定是搜不到的,一搜出来保证是魔蝎座啥的。也是在这儿我才知道github的强大,感觉在写组件的这几天,阅读英文文档的能力有所提升啊,领悟太晚,罪过罪过。感觉这是我目前的瓶颈之一——不能合理利用资源,瓶颈老在那儿卡着很不舒服啊,得想办法突破,突破的过程固然是比较困难的,限于能力,之前从来没写过这些东西,有时候有点想放弃的念头,在这儿又想起那一句话:“不是你不能,而是你对自己的要求太低。” 老大也说过:“不能呆几个月写的全是自己写过的东西啊!”,一想回来,咦,目前对自己的要求是有点低,这么早下班回去干嘛啊,还不如在公司加会儿班,还有饭吃,还可以利用公司的资源学习,提升自己的能力,不加班真是太亏了(作为单身狗算是自我安慰吧,特么,明天5月20号,又是虐狗节!)!再坚持一下,网上各种查资料,功夫不负有心人,成功了!


正题

上面说了,IE6-9依赖的是mOxie,在低版本的IE下,mOxie判断当前运行环境,利用了Flash或者Silverlight与服务器交互,实现文件异步上传,这儿有个坑注意下:如果flash文件或者xap文件放到CDN上,然后在不同域使用mOxie的话,会出现跨域的情况,Flash在跨域调用的时候会检查crossdomain.xml来判断当前域是否安全,这儿记得在服务器端设置一下。

当然,图片处理,mOxie这么强大的工具肯定是支持的,具体用法看官方文档额!在支持H5的浏览器下,我们需要按需加载,我们不想依赖mOxie来处理,这得要我们自己处理了。我的思路是:压缩之前,把EXIF信息从源文件取出来,并不关心EXIF里面具体是什么东西,只管拿出来就行,然后将源文件压缩,然后再把刚才取出来的EXIF信息插入到压缩后的图片里面就行了。

流程图:

  • 获取图片并且展示出来:

要处理图片我们得拿到图片吧,利用H5的File API我们就可以做这个事情了!没错就是FileReader,顾名思义,就是文件读取者了。官方解释:

使用FileReader对象,web应用程序可以异步的读取存储在用户计算机上的文件(或者原始数据缓冲)内容,可以使用File对象或者Blob对象来指定所要处理的文件或数据.其中File对象可以是来自用户在一个input元素上选择文件后返回的FileList对象,也可以来自拖放操作生成的 DataTransfer对象,还可以是来自在一个HTMLCanvasElement上执行mozGetAsFile()方法后的返回结果.

首先得创建一个FileReader实例:

var fileReader = new FileReader();

方法概述:

void readAsArrayBuffer(in Blob blob);

void readAsDataURL(in Blob blob);

事件处理:
onload 当读取操作成功完成时调用.

读取图片并且预览:

html:

<input type="file" id="filedom" />
<img src="#" width="200" id="img">
<br>
<button id="btn">获取</button>

js:

var filedom = document.getElementById("filedom");
var imgdom = document.getElementById("img");
var btn = document.getElementById("btn");

btn.onclick = function(){            
    var imgFile = filedom.files[0];

    if(!imgFile){    
        alert("请选择图片文件!");
        return false;
    }

    showImage(imgFile,function(src){
        imgdom.src = src;
    });

}

function showImage(file,callback){
    var reader = new FileReader();
    reader.onload = function(){
        callback(reader.result);
    }
    reader.readAsDataURL(file);
}

执行结果:

上面代码已经可以读取图片了哦!

  • 利用canvas压缩图片:
    查看canvas api,可以看到 drawImage 函数,画图函数,把图用canvas画出来,然后再用 toBlob 将绘制好的图片转换成一个二进制对象。暂且把这个blob暂存起来待会儿再用。

然而一切并不那么顺利,每个浏览器对toBlob处理方式有所差异,这里得做兼容处理!
HTMLCanvasElement.prototype.toBlob 函数兼容代码:

(function() {
    var CanvasPrototype = window.HTMLCanvasElement &&
        window.HTMLCanvasElement.prototype,
        hasBlobConstructor = window.Blob && (function() {
            try {
                return Boolean(new Blob());
            } catch (e) {
                return false;
            }
        }()),
        hasArrayBufferViewSupport = hasBlobConstructor && window.Uint8Array &&
        (function() {
            try {
                return new Blob([new Uint8Array(100)]).size === 100;
            } catch (e) {
                return false;
            }
        }()),
        BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder ||
        window.MozBlobBuilder || window.MSBlobBuilder,
        dataURLtoBlob = (hasBlobConstructor || BlobBuilder) && window.atob &&
        window.ArrayBuffer && window.Uint8Array && function(dataURI) {
            var byteString,
                arrayBuffer,
                intArray,
                i,
                mimeString,
                bb;
            if (dataURI.split(",")[0].indexOf("base64") >= 0) {
                // Convert base64 to raw binary data held in a string:
                byteString = atob(dataURI.split(",")[1]);
            } else {
                // Convert base64/URLEncoded data component to raw binary data:
                byteString = decodeURIComponent(dataURI.split(",")[1]);
            }
            // Write the bytes of the string to an ArrayBuffer:
            arrayBuffer = new ArrayBuffer(byteString.length);
            intArray = new Uint8Array(arrayBuffer);
            for (i = 0; i < byteString.length; i += 1) {
                intArray[i] = byteString.charCodeAt(i);
            }
            // Separate out the mime component:
            mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0];
            // Write the ArrayBuffer (or ArrayBufferView) to a blob:
            if (hasBlobConstructor) {
                return new Blob(
                    [hasArrayBufferViewSupport ? intArray : arrayBuffer], {
                        type: mimeString
                    }
                );
            }
            bb = new BlobBuilder();
            bb.append(arrayBuffer);
            return bb.getBlob(mimeString);
        };
    if (window.HTMLCanvasElement && !CanvasPrototype.toBlob) {
        if (CanvasPrototype.mozGetAsFile) {
            CanvasPrototype.toBlob = function(callback, type, quality) {
                if (quality && CanvasPrototype.toDataURL && dataURLtoBlob) {
                    callback(dataURLtoBlob(this.toDataURL(type, quality)));
                } else {
                    callback(this.mozGetAsFile("blob", type));
                }
            };
        } else if (CanvasPrototype.toDataURL && dataURLtoBlob) {
            CanvasPrototype.toBlob = function(callback, type, quality) {
                callback(dataURLtoBlob(this.toDataURL(type, quality)));
            };
        }
    }
})();

图片压缩代码:

function imageResize(img, width, height, quality,callback) {
    var type = "image/jpeg";
    var canvas = document.createElement("canvas"),
        ctx = canvas.getContext("2d");
    // quality = options.quality || 0.8;
    canvas.width = width;
    canvas.height = height;
    ctx.drawImage(img, 0, 0, width, height);
    canvas.toBlob(callback, type, quality);
}
  • 取出EXIF信息,这里有点蛋疼,研究半天呢。。。这得去看JPEG图片编码规律,请戳这儿:JPEG文件编/解码详解
    先看图:
    exif

整个JPEG图片的组成。
用sublime打开一个标准的JPEG图片,就可以看到:

每个JPEG图片文件都以 0xffd8 开始,称作SOI(Start of Image),以 0xffd9 结束,称作EOI(End of Image),EXIF 每一项都有一个具体的值,记录格式如下:

0xff+标记号(1个字节) + 数据大小描述符(2个字节)+数据内容(n个字节)

通过这个格式,我们就可以通过读取二进制文件把整个EXIF信息取出来了!

首先我们得利用规律编码格式把里面的标记以及值等分割开来:

/*
 * @param rawImageArray{ArrayBuffer|Array|Blob}  原始图
 * @param callback{Function} 回调函数
 */
function getSegments(rawImage, callback) {
    if (rawImage instanceof Blob) {
        var that = this;
        var fileReader = new FileReader();
        fileReader.onload = function() {
            that.getSegments(fileReader.result, callback);
        };
        fileReader.readAsArrayBuffer(rawImage);
    } else {
        if (!rawImage.length && !rawImage.byteLength) {
            return [];
        }
        var head = 0,
            segments = [];
        var length,
            endPoint,
            seg;
        var arr = [].slice.call(new Uint8Array(rawImage), 0);

        while (1) {
            if (arr[head] === 0xff && arr[head + 1] === 0xda) { //Start of Scan 0xff 0xda  SOS
                break;
            }

            if (arr[head] === 0xff && arr[head + 1] === 0xd8) { //Start of Image 0xff 0xd8  SOI
                head += 2;
            } else { //找到每个marker
                length = arr[head + 2] * 256 + arr[head + 3]; //每个marker 后 的两个字节为 该marker信息的长度
                endPoint = head + length + 2;
                seg = arr.slice(head, endPoint); //截取信息
                head = endPoint;
                segments.push(seg); //将每个marker + 信息 push 进去。
            }
            if (head > arr.length) {
                break;
            }
        }
        callback(segments);
    }
}

然后取出EXIF信息:

/*
 * @param segments{Array|Uint8Array} 处理后的segments
 */
function getEXIF(segments) {
    if (!segments.length) {
        return [];
    }
    var seg = [];
    for (var x = 0; x < segments.length; x++) {
        var s = segments[x];
        //TODO segments
        if (s[0] === 0xff && s[1] === 0xe1) { // app1 exif 0xff 0xe1
            seg = seg.concat(s);
        }
    }
    return seg;
}

将来取出的EXIF信息插入到压缩后的图片中去:

/*
 * @param resizedImg{ArrayBuffer|Blob} 压缩后的图片
 * @param exifArr{Array|Uint8Array} EXIF信息数组
 * @param callback{Function} 回调函数
 */
function insertEXIF(resizedImg, exifArr, callback) {
    if (resizedImg instanceof Blob) {
        var that = this;
        var fileReader = new FileReader();
        fileReader.onload = function() {
            that.insertEXIF(fileReader.result, exifArr, callback);
        };
        fileReader.readAsArrayBuffer(resizedImg);
    } else {
        var arr = [].slice.call(new Uint8Array(resizedImg), 0);
        if (arr[2] !== 0xff || arr[3] !== 0xe0) {
            // throw new Error("Couldn't find APP0 marker from resized image data.");
            return resizedImg; //不是标准的JPEG文件
        }

        var app0_length = arr[4] * 256 + arr[5]; //两个字节

        var newImage = [0xff, 0xd8].concat(exifArr, arr.slice(4 + app0_length)); //合并文件 SOI + EXIF + 去除APP0的图像信息

        callback(new Uint8Array(newImage));
    }
}

代码整理一下:

var ImageTool = {
    /*
     * @param rawImageArray{ArrayBuffer|Array|Blob}
     */
    getSegments: function(rawImage, callback) {
        if (rawImage instanceof Blob) {
            var that = this;
            var fileReader = new FileReader();
            fileReader.onload = function() {
                that.getSegments(fileReader.result, callback);
            };
            fileReader.readAsArrayBuffer(rawImage);
        } else {
            if (!rawImage.length && !rawImage.byteLength) {
                return [];
            }
            var head = 0,
                segments = [];
            var length,
                endPoint,
                seg;
            var arr = [].slice.call(new Uint8Array(rawImage), 0);

            while (1) {
                if (arr[head] === 0xff && arr[head + 1] === 0xda) { //Start of Scan 0xff 0xda  SOS
                    break;
                }

                if (arr[head] === 0xff && arr[head + 1] === 0xd8) { //Start of Image 0xff 0xd8  SOI
                    head += 2;
                } else { //找到每个marker
                    length = arr[head + 2] * 256 + arr[head + 3]; //每个marker 后 的两个字节为 该marker信息的长度
                    endPoint = head + length + 2;
                    seg = arr.slice(head, endPoint); //截取信息
                    head = endPoint;
                    segments.push(seg); //将每个marker + 信息 push 进去。
                }
                if (head > arr.length) {
                    break;
                }
            }
            callback(segments);
        }
    },
    /*
     * @param resizedImg{ArrayBuffer|Blob}
     * @param exifArr{Array|Uint8Array}
     */
    insertEXIF: function(resizedImg, exifArr, callback) {
        if (resizedImg instanceof Blob) {
            var that = this;
            var fileReader = new FileReader();
            fileReader.onload = function() {
                that.insertEXIF(fileReader.result, exifArr, callback);
            };
            fileReader.readAsArrayBuffer(resizedImg);
        } else {
            var arr = [].slice.call(new Uint8Array(resizedImg), 0);
            if (arr[2] !== 0xff || arr[3] !== 0xe0) {
                // throw new Error("Couldn't find APP0 marker from resized image data.");
                return resizedImg; //不是标准的JPEG文件
            }

            var app0_length = arr[4] * 256 + arr[5]; //两个字节

            var newImage = [0xff, 0xd8].concat(exifArr, arr.slice(4 + app0_length)); //合并文件 SOI + EXIF + 去除APP0的图像信息

            callback(new Uint8Array(newImage));
        }
    },
    /*
     * @param segments{Array|Uint8Array}
     */
    getEXIF: function(segments) {
        if (!segments.length) {
            return [];
        }
        var seg = [];
        for (var x = 0; x < segments.length; x++) {
            var s = segments[x];
            //TODO segments
            if (s[0] === 0xff && s[1] === 0xe1) { // app1 exif 0xff 0xe1
                seg = seg.concat(s);
            }
        }
        return seg;
    },
    /*
     *@param base64{String}
     */
    decode64: function(base64) {
        var b64 = "data:image/jpeg;base64,";
        if (base64.slice(0, 23) !== b64) {
            return [];
        }
        var binStr = window.atob(base64.replace(b64, ""));
        var buf = new Uint8Array(binStr.length);
        for (var i = 0, len = binStr.length; i < len; i++) {
            buf[i] = binStr.charCodeAt(i);
        }
        return buf;
    },
    /*
     *@param arr{Array}
     */
    encode64: function(arr) {
        var data = "";
        for (var i = 0, len = arr.length; i < len; i++) {
            data += String.fromCharCode(arr[i]);
        }
        return "data:image/jpeg;base64," + window.btoa(data);
    }
};

最终的代码:
把上面的代码综合一下,完成图片压缩并且保存EXIF:

var filedom = document.getElementById("file");
var imgdom = document.getElementById("img");
var btn = document.getElementById("btn");

btn.onclick = function(){            
    var imgFile = filedom.files[0];

    if(!imgFile){    
        alert("请选择图片文件!");
        return false;
    }

    showImage(imgFile,function(src){
        imgdom.src = src;
        imgdom.onload = function(){
            imageResize(imgdom,400,225,1,function(blob){
                ImageTool.getSegments(imgFile,function(segments){
                    var exif = ImageTool.getEXIF(segments);//获取exif信息
                    ImageTool.insertEXIF(blob,exif,function(newImage){
                            showImage(new Blob([newImage],{type : "image/jpeg"}),function(src){
                                var img = new Image();
                                img.src = src;
                                document.body.appendChild(img);
                            });
                    });
                });//获取 分割 segments
            });
        }
    });

}

function showImage(file,callback){
    var reader = new FileReader();
    reader.onload = function(){
        callback(reader.result);
    }
    reader.readAsDataURL(file);
}

function imageResize(img, width, height, quality,callback) {
    var type = "image/jpeg";
    var canvas = document.createElement("canvas"),
        ctx = canvas.getContext("2d");
    // quality = options.quality || 0.8;
    canvas.width = width;
    canvas.height = height;
    ctx.drawImage(img, 0, 0, width, height);
    canvas.toBlob(callback, type, quality);
}

运行结果:

尺寸对比:

EXIF信息对比:
压缩前
压缩后

附件:

压缩前的图片:http://icaifeimg.qiniudn.com/nokia.jpg (2.2mb)

压缩后的图片:http://icaifeimg.qiniudn.com/nokia-compressed-width-exif.jpg (205kb)
  • 参考

    http://code.flickr.net/2012/06/01/parsing-exif-client-side-using-javascript-2/
    http://code.ciaoca.com/javascript/exif-js/
    http://blog.csdn.net/yyjsword/article/details/28876739