zoukankan      html  css  js  c++  java
  • H5拍照应用开发经历的那些坑儿

    一、项目简介

    1.1、项目背景:
    这是一个在移动终端创新应用的项目,用户在浏览器端(微信/手Q)即可完成与金秀贤的合影,希望通过这样一种趣味体验,引发用户的分享与转发的热潮。

    1.2、系统要求:
    ios6-ios7、android3.0-android4.3、android4.4+(非webview内)

    1.3、体验地址:

    二、初步技术方案确定

    在项目前期首先启动了技术预演,确定初步技术方案(*非最终解决方案):

    2.1、获取用户照片数据
    2.1.1、首先放弃了主动获取用户摄像头的getUserMedia,因为移动终端支持率太低;
    2.1.2、确定使用Input控件获取照片文件、使用FileReader读取照片数据,android3.0+、ios6.0+都可以支持。

    2.2、编辑合成照片
    2.2.1、放弃使用canvas编辑(即将图像数据读取到canvas内进行处理)照片,考虑到开发成本成高;
    2.2.2、选用dom编辑(img标签),然后使用html2canvas,方便保存数据。

    2.3、保存并上传照片
    确定使用canvas的toDataURL接口,提交base64数据到服务器。

    三、碰到的那些坑儿

    按照既定的技术方案开始执行,开始碰到一个个问题,有些问题可以绕过,有些问题只能推倒重来。

    3.1、照片方向反了(如下图所示)

    问题描述:
    手持设备不同方向所拍摄的照片方向不同,照片的方向都会不同,但相册中会自动纠正,这一问题在ios和android中都存在。
    问题解决:
    3.1.1、将图片数据转换成二进制数据,方便获取图片的exif信息;(这里我引入了 Binary Ajax
    3.1.2、获取图片的exif信息;(这里我使用了 Javascript EXIF Reader
    3.1.3、通过图片exif信息,获取图片拍摄时所持设备方向orientation。
    关键代码:

    // 读取图片数据
    var fr = new FileReader();
    fr.readAsDataURL(file); 
    
    fr.onload = function(fe){ 
    	var result = this.result;
    	var img = new Image();
           var exif;
    	img.onload = function() {
    		var orientation  = exif ? exif.Orientation  : 1;
    		// 判断拍照设备持有方向调整照片角度
    		switch(orientation) {
    			case 3: 
    				imgRotation = 180; 
    				break;
    			case 6: 
    				imgRotation = 90; 
    				break;
    			case 8: 
    				imgRotation = 270; 
    			break;
    		}
    	};
    
    	// 转换二进制数据
    	var base64 = result.replace(/^.*?,/,'');
    	var binary = atob(base64); 
    	var binaryData = new BinaryFile(binary);
    
    	// 获取exif信息
    	exif = EXIF.readFromBinaryFile(binaryData);
    
    	img.src = result;
    };
    

    3.2、html2canvas问题重重
    问题背景:
    为什么要用html2canvas呢,因为我们需要将用户合成照片后的base64数据提交服务器,所以我们需要通过转换为canvas获取照片数据。
    问题详情:
    3.2.1、图片使用css3 transform旋转了图片方向,但最终html2canvas渲染结果却未保存旋转信息;
    3.2.2、html2canvas的渲染起点为网页右上角,而且不能更改设置;
    3.2.3、ios大图被压扁了。
    问题解决:
    但最终因为碰到太多无法绕过的问题,不得不放弃html2canvas的方案,全部转为canvas开发。

    3.3、ios大图被压扁了
    问题详情:
    当照片超过2M时,ios会出现压扁的情况(如下图所示)


    问题解决:
    获取图片实际比例,重置图片的比例。(stack overflow讨论帖
    需要注意的是,ratio的获取是通过检测像素alpha,需要过滤png图片,这在stack overflow的讨论上没有人提出。
    关键代码:

    var getRatio = function(img) {
        if(/png$/i.test(img.src)) {
            return 1;
        }
        var iw = img.naturalWidth, ih = img.naturalHeight;
        var canvas = document.createElement('canvas');
        canvas.width = 1;
        canvas.height = ih;
        var ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0);
        var data = ctx.getImageData(0, 0, 1, ih).data;
        var sy = 0;
        var ey = ih;
        var py = ih;
        while (py > sy) {
            var alpha = data[(py - 1) * 4 + 3];
            if (alpha === 0) {
                ey = py;
            } else {
                sy = py;
            }
            py = (ey + sy) >> 1;
        }
        var ratio = (py / ih);
        return (ratio===0)?1:ratio;
    }
    

    3.4、照片太模糊啦,我想提高精度!
    问题描述:


    如上图所示,为了减少本地内存消耗,项目最初采用尺寸是320×270。在项目上线后,在确保内存占用不过高的情况下,开始尝试开发高清方案,测试地址如下:

    在主流设备上测试,性能并无太大问题,但当网络切换为3g时,测试图片合并上传时间8-12s,是原来时间的3倍左右,于是测试了一下3g网络的上传速度:

     
    下载速度
    上传速度
    联通3g
    220kb/s
    80kb/s
    电信3g
    180kb/s
    60kb/s
    移动3g
    100kb/s
    13kb/s
    移动2g
    15kb/s
    12kb/s

    平常会留意用户的下载速度,但对上传速度没太在意,640×540图片的base64数据大小为120kb左右,加上延时,3g环境下平均上传时间是5s左右。于是,上传速度成为了高清方案的瓶颈。

    解决方案:
    3.4.1、在微信和手Q环境中检测用户环境如果为wifi,则启用高清方案,但由于在这个网站推广的渠道很多,环境复杂,并不能完全解决问题,所以放弃了该解决方式;
    3.4.2、在上传前对base64数据进行文本压缩,目前正在尝试lz77压缩,未上线。

    3.5、canvas toDataURL bug
    问题描述:
    已测试,至少在手机QQ浏览器中,canvas对象使用toDataURL方法获取不到任何数据。
    问题解决:
    使用JPEGEncoder将图片像素数据转换为base64数据。
    关键代码:

    _public.toDataURL = function(callback){
        var self = this;
        // 去除编辑状态的元素
        self.unSelect();
     
        // 已测手机QQ浏览器canvas.toDataURL有问题,使用jeegEncoder
        window.setTimeout(function(){
            var encoder = new JPEGEncoder();
            var data = encoder.encode(self.canvas.getContext('2d').getImageData(0,0,self.stage.width,self.stage.height), 90);
            callback.call(self, data);
        }, 1000/self.config.fps)
    }
    

    3.6、当getElementOffset遇上transform
    问题代码:

    Quark.getElementOffset = function(elem)
    {
        var left = elem.offsetLeft, top = elem.offsetTop;
        while((elem = elem.offsetParent) && elem != document.body && elem != document)
        {
            left += elem.offsetLeft;
            top += elem.offsetTop;
        }
        return {left:left, top:top};
    };
    

    问题描述:
    当目标元素的上级元素中有使用transform:translate(x,y)时,用如上的方法都会导致offset计算错误,这一bug在常用canvas框架EaseJSQuarkJS,DOM类库Zepto中都存在。我项目中使用的是QuarkJS,碰到具体问题是舞台事件坐标不正确,由于是框架中的bug,足足花了半天时间才追查到。
    问题解决:
    offsetLeft或offsetTop需要减去translate的差值。

    四、项目总结

    4.1、最终技术方案
    4.1.1、获取用户照片数据
    使用Input控件获取照片文件、使用FileReader读取照片数据,android3.0+、ios6.0+都可以支持。
    4.1.2、编辑合成照片
    4.1.2.1、使用canvas编辑图片,使用canvas框架为QuarkJS;
    4.1.2.2、使用binaryajax和exif获取照片信息,用于解决ios bug和照片方向调整;
    4.1.3、保存并上传照片
    4.1.3.1、使用JPEGEncoder转换为base64数据;
    4.1.3.2、使用lz77进行数据压缩

    4.2、心得
    这个项目进行得并不顺利,经历过1次推翻整体方案重写、1次框架bug纠错、多次系统和浏览器的bug修复,由于线上并没有此类相对成熟的应用,找不到可参考案例,吐槽之余,也总结出一些心得:
    4.2.1、对于创新类的应用,前期技术预演很关键,不能只是探索可行性;
    4.2.2、选择一个成熟的框架很关键,QuarkJS虽然本身架构不错并且很轻量,但使用它的过程中还是碰到过不少bug或不完善之处,并且文档不详细;
    4.2.3、需要善于利用现有技术。这个项目中使用了不少第三方框架来解决特定问题,如果没有这些,项目周期将会相当长。
    4.2.4、H5从图像到音频到视频,还有太多领域值得探索,有很大可挖掘的价值,想想都有点小兴奋呢!

    4.3、图片编辑类整体代码

    /**
     * @author Brucewan
     * @version 1.0
     * @date 2014-07-11
     * @description 图片编辑器
     * @extends tg.Base
     * @name tg.ImageEditor
     * @requires zepto.js
     * @requires base.js
     * @class
    */
    tg.add('tg.ImageEditor:tg.Base', function() {
     
        /**
         * public 作用域
         * @alias tg.ImageEditor#
         * @ignore
         */
        var _public = this;
     
        var _private = {};
     
        /**
         * public static作用域
         * @alias tg.ImageEditor.
         * @ignore
         */
        var _static = this.constructor;
         
     
        _public.constructor = function(config) {
            this.config = Zepto.extend(true, {}, _static.config, config); // 参数接收
            this.init();
        }
     
        // 插件默认配置
        _static.config = {
             320,
            height: 320,
            fps: 60
        };
     
     
        /***
         * 初始化
         * @description 参数处理
         */
        _public.init = function(){
            var self = this;
            var config = self.config;
     
            // 自定义事件绑定
            self.effect && self.on(self.effect);
            config.event && self.on(config.event);
     
            if(self.trigger('beforeinit') === false) {
                return;
            }
     
            // 创建canvas
            var canvas = Quark.createDOM('canvas', {
                 config.width, 
                height: config.height, 
                style: {backgroundColor:"#fff"}
            }); 
            canvas = $(canvas).appendTo(config.container)[0];
     
     
     
            var context = new Quark.CanvasContext({canvas:canvas});
            self.stage = new Quark.Stage({config.width, height:config.height, context:context});  
            self.canvas = canvas;
            self.context = context;
     
            // register stage events
            var em = this.em = new Quark.EventManager();
            em.registerStage(self.stage, ['touchstart', 'touchmove', 'touchend'], true, true);
            self.stage.stageX = config.stageX !== window.undefined ? config.stageX : self.stage.stageX;
            self.stage.stageY = config.stageY !== window.undefined ? config.stageY : self.stage.stageY;
     
            var timer = new Quark.Timer(1000/config.fps);
            timer.addListener(self.stage);
            timer.addListener(Quark.Tween);
            timer.start();
     
            var bg = new Q.Graphics({config.width, height:config.height});
            bg.beginFill("#fff").drawRect(0, 0, config.width, config.height).endFill().cache();
            self.stage.addChild(bg)
     
            _private.attach.call(self);
        };
     
     
     
        _private.attach = function(){
            var self = this;
            var config = self.config;
     
            config.trigger.on('change', function(e){
                self.trigger('beforechange');
     
                // 只上传一个文件
                var file = this.files[0]; 
     
     
                // 限制上传图片文件
                if(file.type && !/image/w+/.test(file.type)){ 
                    alert('请选择图片文件!'); 
                    return false; 
                } 
     
                var fr = new FileReader();
                fr.readAsDataURL(file); 
     
                 
     
                fr.onload = function(fe){ 
                    var result = this.result;
                    var img = new Image();
                                var exif;
                    img.onload = function() {
                        self.addImage({img: img, exif: exif});
                        self.trigger('change');
                    };
                            // 转换二进制数据
                            var base64 = result.replace(/^.*?,/,'');
                            var binary = atob(base64);
                            var binaryData = new BinaryFile(binary);
     
                            // get EXIF data
                            exif = EXIF.readFromBinaryFile(binaryData);
     
                    img.src = result;
     
                };
     
                 
                 
            });
     
     
            self.stage.addEventListener('touchstart', function(e){
                if(self.imgs) {
                    for(var i = 0; i < self.imgs.length; i++) {
                        self.imgs[i].disable();
                    }
                }
                if(e.eventTarget && e.eventTarget.parent.enEditable) {
                    e.eventTarget.parent.enEditable();
                    self.activeTarget = e.eventTarget.parent;
                }
            });
            self.stage.addEventListener('touchmove', function(e){
                var touches = e.rawEvent.touches ||  e.rawEvent.changedTouches;
                if(e.eventTarget && (e.eventTarget.parent == self.activeTarget) && touches[1]) {
                    var dis = Math.sqrt(Math.pow(touches[1].pageX - touches[0].pageX, 2) + Math.pow(touches[1].pageY - touches[0].pageY, 2) );
                    if(self.activeTarget.mcScale.touchDis) {
                        var scale = dis / self.activeTarget.mcScale.touchDis -1;
                        if( self.activeTarget.getCurrentWidth() < 100 && scale < 0) {
                            scale = 0;
                        }
     
                        self.activeTarget.scaleX += scale;
                        self.activeTarget.scaleY += scale;
                    } 
                    self.activeTarget.mcScale.touchDis = dis;
                }
            });
            self.stage.addEventListener('touchend', function(){
                if(self.activeTarget && self.activeTarget.mcScale) {
                    delete  self.activeTarget.mcScale.touchDis;
                }
            });
     
     
        };
     
        _public.addImage = function(info){
            var self = this;
            var config = self.config;
            var img = info.img;
            var exif = info.exif;
            var imgContainer;
            var mcScale;
            var mcClose;
            var imgWidth = img.width;
            var imgHeight = img.height;
            var imgRotation = 0;
            var imgRegX = 0;
            var imgRegY = 0;
            var imgX = 0;
            var imgY = 0;
            var posX = info.pos ? info.pos[0] : 0;
            var posY = info.pos ? info.pos[1] : 0;
            var imgScale = 1;
            var orientation  = exif ? exif.Orientation  : 1;
            var getRatio = function(img) {
                if(/png$/i.test(img.src)) {
                    return 1;
                }
                var iw = img.naturalWidth, ih = img.naturalHeight;
                var canvas = document.createElement('canvas');
                canvas.width = 1;
                canvas.height = ih;
                var ctx = canvas.getContext('2d');
                ctx.drawImage(img, 0, 0);
                var data = ctx.getImageData(0, 0, 1, ih).data;
                var sy = 0;
                var ey = ih;
                var py = ih;
                while (py > sy) {
                    var alpha = data[(py - 1) * 4 + 3];
                    if (alpha === 0) {
                        ey = py;
                    } else {
                        sy = py;
                    }
                    py = (ey + sy) >> 1;
                }
                var ratio = (py / ih);
                return (ratio===0)?1:ratio;
            }
            var ratio = getRatio(img);
     
     
            // window.setTimeout(function(){
            //  alert(imgContainer.width);
            //  alert(img);
            // }, 5000)
     
     
     
     
            if(typeof img == 'string') {
                var url = img;
                img = new Image();
                img.src = url;
            }
     
     
            // 判断拍照设备持有方向调整照片角度
            switch(orientation) {
                case 3: 
                    imgRotation = 180; 
                    imgRegX = imgWidth;
                    imgRegY = imgHeight * ratio;
                    // imgRegY -= imgWidth * (1-ratio);
                    break;
                case 6: 
     
                    imgRotation = 90; 
                    imgWidth = img.height;
                    imgHeight = img.width;
                    imgRegY = imgWidth * ratio ;
                    // imgRegY -= imgWidth * (1-ratio);
                    break;
                case 8: 
                    imgRotation = 270; 
                    imgWidth = img.height;
                    imgHeight = img.width;
                    imgRegX = imgHeight * ratio;
     
                    if(/iphone|ipod|ipad/i.test(navigator.userAgent)) {
                        alert('苹果系统下暂不支持你以这么萌!萌!达!姿势拍照!');
                        return;
                    }
     
                break;
     
     
            }
            imgWidth *= ratio;
            imgHeight *= ratio;
     
     
            if(imgWidth > self.stage.width) {
                imgScale = self.stage.width / imgWidth;
            }
     
            imgWidth = imgWidth * imgScale;
            imgHeight = imgHeight * imgScale;
     
            imgContainer = new Quark.DisplayObjectContainer({ imgWidth, height: imgHeight});
            imgContainer.x = posX;
            imgContainer.y = posY;
     
     
            img = new Quark.Bitmap({image:img, regX:imgRegX, regY:imgRegY});
            img.rotation = imgRotation;
            img.x = imgX;
            img.y = 0;
            img.scaleX = imgScale * ratio;
            img.scaleY = imgScale;
     
     
     
     
     
            if(config.iconScale && !info.disScale) {
                var iconScaleImg = new Image();
                iconScaleImg.onload = function(){
                    var rect = config.iconScale.rect;
                    mcScale = new Quark.MovieClip({image:iconScaleImg});
                    mcScale.addFrame([{rect: rect}]);
                    mcScale.x = imgWidth - rect[2];
                    mcScale.y = 0;
                    mcScale.alpha = 0.5;
                    mcScale.visible = false;
                    mcScale.addEventListener('touchstart', function(e){
                        mcScale.scaleable = true;
                        mcScale.startX = e.eventX;
                        mcScale.startY = e.eventY;
                        mcScale.alpha = 0.8;
                        var curW = imgContainer.getCurrentWidth();
                        var scaleMove = function(e){
                            if(mcScale.scaleable) {
                                // 缩放
                                var disX = e.eventX - mcScale.startX;
                                var scaleX = (curW+disX)/imgContainer.width;
     
                                if( imgContainer.getCurrentWidth() < 100 && imgContainer.scaleX > scaleX) {
                                    return;
                                }
     
                                imgContainer.scaleX = scaleX;
                                imgContainer.scaleY = scaleX;
     
                                // 旋转
                                var disOriX = e.eventX - imgContainer.x;
                                var disOriY = e.eventY- imgContainer.y;
                                var rotate = Math.atan2(disOriY,disOriX) * 360 / (2 * Math.PI);
                                imgContainer.rotation = parseInt(rotate/1)*1;
                            }
                        };
                        var scaleEnd = function(e) {
                            mcScale.scaleable = false;
                            mcScale.alpha = 0.5;
                            self.stage.removeEventListener('touchmove', scaleMove);
                            self.stage.removeEventListener('touchend', scaleEnd);
                        }
                        self.stage.addEventListener('touchmove', scaleMove);
                        self.stage.addEventListener('touchend', scaleEnd);
                    });
                    imgContainer.mcScale = mcScale;
                    imgContainer.addChild(mcScale);
                };
                iconScaleImg.src = config.iconScale.url;
            }
     
            var border = new Q.Graphics({imgWidth+10, height:imgHeight+10, x:-5, y:-5});
            border.lineStyle(5, "#aaa").beginFill("#fff").drawRect(5, 5, imgWidth, imgHeight).endFill().cache();
            border.alpha = 0.5;
            border.visible = false;
            imgContainer.addChild(border);
     
            if(config.iconClose) {
                var iconCloseImg = new Image();
                iconCloseImg.onload = function(){
                    var rect = config.iconClose.rect;
                    mcClose = new Quark.MovieClip({image:iconCloseImg});
                    mcClose.addFrame([{rect: rect}]);
                    mcClose.x = 0;
                    mcClose.y = 0;
                    mcClose.alpha = 0.5;
                    mcClose.visible = false;
                    mcClose.addEventListener('touchstart', function(e){
                        mcClose.alpha = 0.8;
                    }); 
                    mcClose.addEventListener('touchend', function(e){
                        self.stage.removeChild(imgContainer);
                    });                                 
                    self.stage.addEventListener('touchend', function(e){
                        mcClose.alpha = 0.5;
                    });
                    imgContainer.addChild(mcClose);
                };
                iconCloseImg.src = config.iconClose.url;
            }
     
     
            if(!info.disMove && !info.disable) {
                img.addEventListener('touchstart', function(e){
                    var fnMove;
                    var fnEnd;
                    // 拖动
                    img.curW = imgContainer.getCurrentWidth();
                    img.curH = imgContainer.getCurrentHeight();
                    img.moveabled = true;
                    img.startX = e.eventX;
                    img.startY = e.eventY;
     
                    fnMove = function(e){
                        // 是否双指按下
                        var isScale = e.rawEvent && e.rawEvent.touches[1];
     
                        if(img.moveabled && !isScale) {
                            var disX = e.eventX - img.startX;
                            var disY = e.eventY - img.startY;
                            var setX = imgContainer.x + disX;
                            var setY = imgContainer.y + disY;
     
                            var diffX = 0, diffY = 0;
     
                            if(setX < -img.curW/2 + 5 && disX < 0) {
                                setX = -img.curW/2;
                            }
                            if(setY < -img.curH/2 + 5 && disY < 0) {
                                setY = -img.curH/2;
                            }
                            if(setX > -img.curW/2 + self.stage.width - 5 && disX > 0) {
                                setX =  self.stage.width - img.curW/2;
                            }
                            if(setY > self.stage.height - 5 && disY > 0) {
                                setY =   self.stage.height;
                            }
     
                            imgContainer.x = setX;
                            imgContainer.y = setY;
                            img.startX = e.eventX;
                            img.startY = e.eventY;
                        }   
                    };
     
                    fnEnd = function(){
                        img.moveabled = false;
                        self.stage.addEventListener('touchmove');
                        self.stage.addEventListener('touchend');    
                    }
                    self.stage.addEventListener('touchmove', fnMove);
                    self.stage.addEventListener('touchend', fnEnd);
     
     
                });
            }
     
     
            imgContainer.enEditable = function(){
                if(info.disable) {
                    return;
                }
                border.visible = true;
                if(mcScale) {
                    mcScale.visible = true;
                }
                if(mcClose) {
                    mcClose.visible = true;
                }
            }
            imgContainer.disable = function(){
                border.visible = false;
                if(mcScale) {
                    mcScale.visible = false;
                }       
                if(mcClose) {
                    mcClose.visible = false;
                }
            }
     
     
            img.update = function(){
                if(imgContainer && imgContainer.scaleX) {
                    if(mcScale && mcScale.scaleX) {
                        mcScale.scaleX = 1/imgContainer.scaleX;
                        mcScale.scaleY = 1/imgContainer.scaleY;
                        mcScale.x = border.getCurrentWidth() - 10 - mcScale.getCurrentWidth();
                    }
                    if(mcClose && mcClose.scaleX) {
                        mcClose.scaleX = 1/imgContainer.scaleX;
                        mcClose.scaleY = 1/imgContainer.scaleY; 
                        mcClose.x = 0;
                    }
                }
     
            }
     
     
            // imgContainer.rotation = 10;
     
            imgContainer.addChild(img);
     
     
            self.stage.update = function(){
                // console.log(0)
                // img.rotation  ++;
            }
     
     
     
     
     
            imgContainer.update = function(){
                // this.rotation  ++;
            }
     
     
            self.stage.addChild(imgContainer);
     
            if(self.imgs) {
                self.imgs.push(imgContainer);
            } else {
                self.imgs = [imgContainer];
            }
             
     
     
     
            // self.imgContainer.addEventListener('touchend', function(){
            //  alert('sss')
            // });
     
            return imgContainer;
     
     
        };
     
        _public.clear = function(){
            if(this.imgs) {
                for(var i = 0; i < this.imgs.length; i++) {
                    this.stage.removeChild(this.imgs[i]);
                }
            }
        };
     
        _public.unSelect = function(){
            var imgs = this.imgs;
            if(imgs) {
                for(var i = 0; i < imgs.length; i++) {
                    imgs[i].disable();
                }
            }
        };
     
    _public.toDataURL = function(callback){
        var self = this;
        // 去除编辑状态的元素
        self.unSelect();
     
        // 已测手机QQ浏览器canvas.toDataURL有问题,使用jeegEncoder
        window.setTimeout(function(){
            var encoder = new JPEGEncoder();
            var data = encoder.encode(self.canvas.getContext('2d').getImageData(0,0,self.stage.width,self.stage.height), 90);
            callback.call(self, data);
        }, 1000/self.config.fps)
    }
     
     
     
     
    });
    

    作者:万技师
    出处:http://www.cnblogs.com/wanbo/
    转载请在明显位置注明原文链接,否则保留追究法律责任的权利。

  • 相关阅读:
    Java自学
    java自学
    每日总结
    每日总结
    每日总结
    每周总结
    每日总结
    每日总结
    每日总结
    每日总结
  • 原文地址:https://www.cnblogs.com/wanbo/p/5762910.html
Copyright © 2011-2022 走看看