New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

移动端Web上传图片实践 #7

Open
xiangpaopao opened this Issue Nov 25, 2014 · 16 comments

Comments

Projects
None yet
@xiangpaopao
Owner

xiangpaopao commented Nov 25, 2014

从iOS 6+、Android 3+开始 (来源http://mobilehtml5.org/),移动端可以通过网页中的<input type="file">来拍照上传或是上传相册中的照片。 不过从图片上传到服务器后可能会遇到图片莫名其妙旋转的问题,如图

default

一些设备在拍照时明明是竖着拍的(右),传到服务端后从图片查看器中看到的是横着的(左),而在一些图片处理工具或是浏览器中通过http协议看到的却是正常的,原因是在照片的exif中的Orientation属性控制了照片的旋转方向。

//Exif 信息示例
Exif.Image.Make                              Ascii       6  Canon
Exif.Image.Model                             Ascii      20  Canon PowerShot S40
Exif.Image.Orientation                       Short       1  top, left
Exif.Image.XResolution                       Rational    1  180
Exif.Image.YResolution                       Rational    1  180
Exif.Image.ResolutionUnit                    Short       1  inch
Exif.Image.DateTime                          Ascii      20  2003:12:14 12:01:44
Exif.Image.YCbCrPositioning                  Short       1  Centered
......

大部分图片查看器和编辑工具都会去根据这个属性控制照片方向,而windows自带的查看器等工具并不理睬他。
为什么要有Orientation属性呢,我并没有找到官方的解释,见到种说法挺有道理:几乎所有的摄像头在出场的时候成相芯片都是有方向的,拍出来的照片的像素都是默认方向的。如果每拍一张照片就对这些像素进行旋转,如果数码相机每秒连拍20张来算,旋转操作将会非常耗时。更聪明的做法是拍照时只记录一个方向,然后显示的时候按方向显示出来即可。因此exif定义了一个标准的方向参数,只要读图的软件都来遵守规则,加载时候读取图片方向,然后做相应的旋转即可。这样既可以达到快速成像的目的,又能达到正确的显示。

orient_flag2

修正图片方向的问题

为了图片显示不出问题,我们得修改Orientation属性。首先想到的是服务端来修正方案,对于图片来说exif信息不是必须的,可以根据Orientation的值来对照片进行手动矫正的操作,然后再去掉exif,类似这样:

switch(exif['Orientation']){
    case 2:
        image->save;
        break;
    case 3:
        image->rotate(-180)->save;
        break;
    case 4:
        image->rotate(180)->save;
        break;
    ······
}

更好的方式是用一些图片工具自动处理,GraphicsMagick和Imagemagick用来处理这个再合适不过了,他俩都可以用对图像进行旋转、裁剪、缩放、替换颜色;添加文本、水印、图形等常见操作,GraphicsMagick是从Imagemagick分支出来的,他俩有着几乎一样的API,可以在命令行工具中轻松操作。这里我用的是GraphicsMagick,他更轻便,易装易用。在Java、PHP、Nodejs等常见后端语言中可以用相关库轻松的操作GraphicsMagick的API。

后端以Nodejs为例。首先你需要在你的机器上安装GraphicsMagick。 然后npm install gm 模块就可以了。gm提供的接口非常友好,你只要

gm('/path/to/img.jpg')
.autoOrient()
.resize(240, 240)
.write('/path/to/new.jpg', function (err) {
  if (err) ...
})

这样就已经完成了图片的自动修正方向和压缩尺寸的工作。

异步上传图片

传统提交表单方式放在今天已经不能忍了,XHR2中支持把文件放在Formdata对象中异步提交,只考虑移动端,就可以舍弃iframe之类的兼容方案了。核心代码这样:

var xhr = new XMLHttpRequest();
var formData = new FormData();
formData.append('file', input.files[0]);
xhr.open('POST', form.action);
xhr.send(formData);

XHR2中还可以通过process事件来监听进度,实现类似进度条的功能

xhr.onprogress = updateProgress;
xhr.upload.onprogress = updateProgress;
function updateProgress(event) {
    if (event.lengthComputable) {
        var percentComplete = event.loaded / event.total;
    ......
  }
}

用FormData发送的请求头中你的Content-Type 会变成这样 multipart/form-data; boundary=----WebKitFormBoundaryyqVkWF3PcCpAzZp9,如果上传时要附带参数也可以直接append到formData里。
然后Nodejs中可以用connect-busboy来接收文件,在express框架中大概是这样:

var express = require('express'),
    http = require('http'),
    fs = require('fs'),
    busboy = require('connect-busboy'),
    gm = require('gm');

var app = express();
app.use(busboy());
.....
app.post('/upload', function(req, res) {
    req.busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
        ......
        file.on('end', function () {
            gm(filePath)
                .autoOrient()
                .thumbnail(200, 200)
                .write(fullname, function(err){
                    if (err) return console.dir(arguments)
                    res.json({
                    ......
                    });
                }
            )
        });
        file.pipe(fs.createWriteStream(filePath));
    });
    req.pipe(req.busboy);
});
......
app.listen(3001);

前端压缩图片

图片上传的主体工作算是完成了,不过现在手机随便拍张照片就是一两兆,wifi环境下不说,移动网络通过这方案上传照片就有点坑了。手机客户端中一般会先压缩图片再上传,Web中如何实现压缩后上传呢?
可以把图片读到canvas中,然后用canvas.toDataURL()接口输出画布的base64编码,再把base64编码转成Blob塞到Formdata里传到后端。这样即可以压缩图片减少流量,又可以在前端就修正图片旋转的问题。(Discuss:直接把base64传到后端是否可行呢,后面试一试)

canvasResize这个库已经把一切封装好了https://github.com/gokercebeci/canvasResize ,同时他依赖Exif.js 修正了因Orientation属性产生的旋转问题。前端主要的代码:

var file = input.files[0];
canvasResize(file, {
        width: 300,
        height: 0,
        crop: false,
        quality: 100,
        callback: function(data, width, height) {
            var blob = canvasResize('dataURLtoBlob', data);
            var form = new FormData();
            form.append('file',blob);
            $.ajax({
                type: 'POST',
                url: server,
                data: form,
                contentType: false,
                processData: false,
            }).done(function (res) {
                ......
            }).fail(function () {
                ......
            }).always(function () {
                ......
            });
        }
});

Nodejs中代码可以参考前面的,继续用connect-busboy模块接收文件。

实际测试一下iOS没问题,Android 4 有些机型不行,貌似修改过file的Blob数据发到服务端的数据字节就会为0 这是安卓的bug https://code.google.com/p/android/issues/detail?id=39882 。 网上有人给出的解决方案是用FileReader把文件读出来,然后把整个二进制文件当请求发到服务端,这种方式要附带参数的话只能放url里了。

var reader = new FileReader();
reader.onload = function() {
    $.ajax({
                type: 'POST',
                url: server,
                data: this.result,
                contentType: false,
                processData: false,
                beforeSend: function (xhr) {
                    xhr.overrideMimeType('application/octet-stream');
            },
            }).done(function (res) {
                ......
            }).fail(function () {
                ......
            }).always(function () {
                ......
            });
};
reader.readAsArrayBuffer(file);

后端在接收这些数据时,会是一段一段的,我是用的拼接的方式处理

app.post('/upload', function(req, res) {
    var imagedata = '';
    req.setEncoding('binary');
    req.on('data', function (chunk) {
        imagedata += chunk
    });
    req.on('end', function (chunk) {
        fs.writeFile(filePath, imagedata, 'binary', function(err){
            if (err) throw err
            res.json({
            ......
            });
        })
    });
});

实测一下,稍低端的的安卓上有点卡,毕竟处理一张图片的运算量可不小,目测目前用前端压缩上传方案的不多,至少微博触屏版 (http://m.weibo.cn/) 就是把原始图片直接上传的,这种方式是否适合直接使用或者还有哪些可以优化的地方有待验证。

这里有一个完整的demo https://github.com/xiangpaopao/mobile-upload-demo 包括上面提到的两种方案
使用的话,依次安装GraphicsMagick > npm install > node app.js

参考

@nimojs

This comment has been minimized.

Show comment
Hide comment
@nimojs

nimojs Feb 5, 2015

感謝分享,這是大坑啊。有一次我们遇到这个问题怪异的哭笑不得。

nimojs commented Feb 5, 2015

感謝分享,這是大坑啊。有一次我们遇到这个问题怪异的哭笑不得。

@Janking

This comment has been minimized.

Show comment
Hide comment
@Janking

Janking May 21, 2015

这个坑终于看到解决方案了。大赞!

Janking commented May 21, 2015

这个坑终于看到解决方案了。大赞!

@WenTao-Love

This comment has been minimized.

Show comment
Hide comment
@WenTao-Love

WenTao-Love Jul 19, 2015

感谢 分享

WenTao-Love commented Jul 19, 2015

感谢 分享

@xyf1096415969

This comment has been minimized.

Show comment
Hide comment
@xyf1096415969

xyf1096415969 Sep 30, 2015

用的是插件吗?是什么插件

xyf1096415969 commented Sep 30, 2015

用的是插件吗?是什么插件

@kujian

This comment has been minimized.

Show comment
Hide comment
@kujian

kujian Nov 23, 2015

移动端不知道可否一次上传多张图片?

kujian commented Nov 23, 2015

移动端不知道可否一次上传多张图片?

@baigao2015

This comment has been minimized.

Show comment
Hide comment
@baigao2015

baigao2015 Feb 21, 2016

粘贴剪切板的时候如何实现图片上传呢?

baigao2015 commented Feb 21, 2016

粘贴剪切板的时候如何实现图片上传呢?

@Garrag

This comment has been minimized.

Show comment
Hide comment
@Garrag

Garrag Mar 12, 2016

找到解决方案了,感谢大牛

Garrag commented Mar 12, 2016

找到解决方案了,感谢大牛

@allan2coder

This comment has been minimized.

Show comment
Hide comment
@allan2coder

allan2coder May 20, 2016

@kujian 我也想知道这个问题,有的手机能多张上传,有的不行。
这个是手机内核问题?

allan2coder commented May 20, 2016

@kujian 我也想知道这个问题,有的手机能多张上传,有的不行。
这个是手机内核问题?

@nimojs

This comment has been minimized.

Show comment
Hide comment
@nimojs

nimojs May 20, 2016

这个问题可以用这个库解决 https://github.com/think2011/localResizeIMG

为什么需要
已踩过很多坑,经过几个版本迭代,以及很多很多网友的反馈帮助、机型测试

图片扭曲、某些设备不自动旋转图片方向,没有jpeg压缩算法..
不支持new Blob,formData构造的文件size为0..
还有某些机型和浏览器(例如QQX5浏览器)莫名其妙的BUG..
按需加载(会根据对应设备自动异步载入JS文件,节省不必要带宽)

原生JS编写,不依赖例如jquery等第三方库,支持AMD or CMD规范。

nimojs commented May 20, 2016

这个问题可以用这个库解决 https://github.com/think2011/localResizeIMG

为什么需要
已踩过很多坑,经过几个版本迭代,以及很多很多网友的反馈帮助、机型测试

图片扭曲、某些设备不自动旋转图片方向,没有jpeg压缩算法..
不支持new Blob,formData构造的文件size为0..
还有某些机型和浏览器(例如QQX5浏览器)莫名其妙的BUG..
按需加载(会根据对应设备自动异步载入JS文件,节省不必要带宽)

原生JS编写,不依赖例如jquery等第三方库,支持AMD or CMD规范。

@zjhui

This comment has been minimized.

Show comment
Hide comment
@zjhui

zjhui Jun 16, 2016

@kujian 手机端可以一次上传多张图片的。把他们都往formdata里面塞,服务端接收一下,循环拿出来就可以了。

zjhui commented Jun 16, 2016

@kujian 手机端可以一次上传多张图片的。把他们都往formdata里面塞,服务端接收一下,循环拿出来就可以了。

@ivanberry

This comment has been minimized.

Show comment
Hide comment
@ivanberry

ivanberry Jan 4, 2017

你好,最近在实践移动端图片上传,读您的文章受益匪浅,感谢,另有问题请教:

是否存在可能上传图片时,只能上传图片库里的图片而不能选择拍照呢?

ivanberry commented Jan 4, 2017

你好,最近在实践移动端图片上传,读您的文章受益匪浅,感谢,另有问题请教:

是否存在可能上传图片时,只能上传图片库里的图片而不能选择拍照呢?

@jiangtao

This comment has been minimized.

Show comment
Hide comment
@jiangtao

jiangtao Jan 4, 2017

@ivanberry

只能上传图片库里的图片而不能选择拍照呢

web前端层面做不到, 需要android ios提供对应的指令,然后把图片的base64给过来。

jiangtao commented Jan 4, 2017

@ivanberry

只能上传图片库里的图片而不能选择拍照呢

web前端层面做不到, 需要android ios提供对应的指令,然后把图片的base64给过来。

@ivanberry

This comment has been minimized.

Show comment
Hide comment
@ivanberry

ivanberry Jan 4, 2017

@Jerret321 感谢,我先问问客户端同事

ivanberry commented Jan 4, 2017

@Jerret321 感谢,我先问问客户端同事

@niexiaofei1988

This comment has been minimized.

Show comment
Hide comment
@niexiaofei1988

niexiaofei1988 Jan 11, 2017

有没有提高性能的呢?是不是使用translate3D开启之后可以加快绘制速度?最近在调使用input上传图片,使用canvas压缩,第二步就是将拍好的照片重新绘制,第三步又增加了按钮,旋转的按钮,PC端做好了,放到手机端(1)图片出不来(2)绘制速度超慢,平均一张图需要5s以上?有没有好的解决方法

niexiaofei1988 commented Jan 11, 2017

有没有提高性能的呢?是不是使用translate3D开启之后可以加快绘制速度?最近在调使用input上传图片,使用canvas压缩,第二步就是将拍好的照片重新绘制,第三步又增加了按钮,旋转的按钮,PC端做好了,放到手机端(1)图片出不来(2)绘制速度超慢,平均一张图需要5s以上?有没有好的解决方法

@CommanderXL

This comment has been minimized.

Show comment
Hide comment
@CommanderXL

CommanderXL Feb 16, 2017

移动端上传图片坑还是挺多的。

比如楼主还没有列出的IOS8,使用readAsDataURL()这个API的获取base64的字符的时候,最后输出的为空字符。

还有部分安卓机下不支持JPEG图片格式的导出等等。

说多了都是泪.

CommanderXL commented Feb 16, 2017

移动端上传图片坑还是挺多的。

比如楼主还没有列出的IOS8,使用readAsDataURL()这个API的获取base64的字符的时候,最后输出的为空字符。

还有部分安卓机下不支持JPEG图片格式的导出等等。

说多了都是泪.

@PLAxiaoxin

This comment has been minimized.

Show comment
Hide comment
@PLAxiaoxin

PLAxiaoxin Mar 15, 2018

gif图过大 手机黑屏碰到过吗?

PLAxiaoxin commented Mar 15, 2018

gif图过大 手机黑屏碰到过吗?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment