博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
H5拍照应用开发经历的那些坑儿
阅读量:7066 次
发布时间:2019-06-28

本文共 16071 字,大约阅读时间需要 53 分钟。

  hot3.png

一、项目简介

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信息;(这里我引入了 )
3.1.2、获取图片的exif信息;(这里我使用了 )
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问题重重
问题背景:
为什么要用 呢,因为我们需要将用户合成照片后的base64数据提交服务器,所以我们需要通过转换为canvas获取照片数据。
问题详情:
3.2.1、图片使用css3 transform旋转了图片方向,但最终html2canvas渲染结果却未保存旋转信息;
3.2.2、html2canvas的渲染起点为网页右上角,而且不能更改设置;
3.2.3、ios大图被压扁了。
问题解决:
但最终因为碰到太多无法绕过的问题,不得不放弃html2canvas的方案,全部转为canvas开发。
3.3、ios大图被压扁了
问题详情: 当照片超过2M时,ios会出现压扁的情况(如下图所示)
问题解决: 
获取图片实际比例,重置图片的比例。( ) 需要注意的是,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、照片太模糊啦,我想提高精度!
问题描述:
如上图所示,为了减少本地内存消耗,项目最初采用尺寸是320x270。在项目上线后,在确保内存占用不过高的情况下,开始尝试开发高清方案,测试地址如下:
在主流设备上测试,性能并无太大问题,但当网络切换为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

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

解决方案:

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

3.5、canvas toDataURL bug

问题描述: 
已测试,至少在手机QQ浏览器中,canvas对象使用toDataURL方法获取不到任何数据。
问题解决: 
使用将图片像素数据转换为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框架
EaseJS
QuarkJS,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 = {		width: 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', {			width: 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({width: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({width: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({width: 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({width: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)}});

转载于:https://my.oschina.net/Seas0n/blog/653213

你可能感兴趣的文章
vue-router 实现分析
查看>>
js如何打印object对象
查看>>
体验javascript之美-第五课 匿名函数自执行和闭包是一回事儿吗?
查看>>
Ruby 2.x 源代码分析:扩展 概述
查看>>
我感觉这是史上最牛的防sql注入方法类
查看>>
angular2开源库收集
查看>>
ArchSummit深圳APM专场总结:性能监控与调优实践干货分享
查看>>
Vue性能优化:如何实现延迟加载和代码拆分?
查看>>
据Progress调查:2018年,70%的客户在使用NoSQL
查看>>
微服务架构适用场景分析
查看>>
OpsRamp推出以服务为中心的AIOps和云监控功能
查看>>
MongoDB又不加密,8.09亿条个人详细记录泄露
查看>>
《引领转型》访谈录
查看>>
用Git虚拟文件系统来解决大型存储问题
查看>>
一行代码迁移TensorFlow 1.x到TensorFlow 2.0
查看>>
明文存密码成惯例?Facebook 6 亿用户密码可被 2 万员工直接看
查看>>
我看到的前端
查看>>
火掌柜iOS端基于CocoaPods的组件二进制化实践
查看>>
强化学习遭遇瓶颈!分层RL将成为突破的希望
查看>>
华泰证券:如何自研高效可靠的交易系统通信框架?
查看>>