正题
上面说了, IE6-9 依赖的是 mOxie https://github.com/moxiecode/moxie , 在低版本的 IE 下, mOxie 判断当前运行环境, 利用了 Flash 或者 Silverlight 与服务器交互, 实现文件异步上传, 这儿有个坑注意下: 如果 flash 文件或者 xap 文件放到 CDN 上, 然后在不同域使用 mOxie 的话, 会出现跨域的情况, Flash 在跨域调用的时候会检查 crossdomain.xml 来判断当前域是否安全, 这儿记得在服务器端设置一下.
当然, 图片处理, mOxie 这么强大的工具肯定是支持的, 具体用法看官方文档额! 在支持 H5 的浏览器下, 我们需要按需加载, 我们不想依赖 mOxie 来处理, 这得要我们自己处理了. 我的思路是: 压缩之前, 把 EXIF http://www.baidu.com/s?wd=exif 信息从源文件取出来, 并不关心 EXIF 里面具体是什么东西, 只管拿出来就行, 然后将源文件压缩, 然后再把刚才取出来的 EXIF 信息插入到压缩后的图片里面就行了.
流程图:
获取图片并且展示出来:
要处理图片我们得拿到图片吧, 利用 H5 的 File API 我们就可以做这个事情了! 没错就是 https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader , 顾名思义, 就是文件读取者了. 官方解释:
使用 FileReader 对象, web 应用程序可以异步的读取存储在用户计算机上的文件 (或者原始数据缓冲) 内容, 可以使用 https://developer.mozilla.org/zh-CN/docs/Web/API/File 对象或者 https://developer.mozilla.org/zh-CN/docs/Web/API/Blob 对象来指定所要处理的文件或数据. 其中 File 对象可以是来自用户在一个 input 元素上选择文件后返回的 https://developer.mozilla.org/zh-CN/docs/Web/API/FileList 对象, 也可以来自拖放操作生成的 DataTransfer https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLCanvasElement 对象, 还可以是来自在一个 https://developer.mozilla.org/zh-CN/docs/Web/API/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 https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLCanvasElement , 可以看到 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 http://www.baidu.com/s?wd=exif 信息, 这里有点蛋疼, 研究半天呢... 这得去看 JPEG 图片编码规律, 请戳这儿: JPEG 文件编 / 解码详解 http://www.cnblogs.com/leaven/archive/2010/04/06/1705846.html ,
先看图:
整个 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://exif.cn/view/4052007/
压缩后 http://exif.cn/view/4052012/
附件:
压缩前的图片: 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
来源: http://icaife.github.io/2015/05/19/js-compress-JPEG-width-exif/