移动端Web上传图片实践

时间:2025-05-15 22:22:09

原文链接

其他链接:

/webplat/info/news_version3/804/808/811/m579/201409/

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

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

//Exif 信息示例
                              Ascii       6  Canon
                             Ascii      20  Canon PowerShot S40
                       Short       1  top, left
                       Rational    1  180
                       Rational    1  180
                    Short       1  inch
                          Ascii      20  2003:12:14 12:01:44
                  Short       1  Centered
......

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

修正图片方向的问题

为了图片显示不出问题,我们得修改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/')
.autoOrient()
.resize(240, 240)
.write('/path/to/', function (err) {
  if (err) ...
})

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

异步上传图片

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

var xhr = new XMLHttpRequest();
var formData = new FormData();
('file', [0]);
('POST', );
(formData);

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

 = updateProgress;
 = updateProgress;
function updateProgress(event) {
    if () {
        var percentComplete =  / ;
......
}
}

用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();
(busboy());
.....
('/upload', function(req, res) {
    ('file', function(fieldname, file, filename, encoding, mimetype) {
        ......
        ('end', function () {
            gm(filePath)
                .autoOrient()
                .thumbnail(200, 200)
                .write(fullname, function(err){
                    if (err) return (arguments)
                    ({
                    ......
                    });
                }
            )
        });
        ((filePath));
    });
    ();
});
......
(3001);

前端压缩图片

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

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

var file = [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();
            ('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 /p/android/issues/detail?id=39882 。 网上有人给出的解决方案是用FileReader把文件读出来,然后把整个二进制文件当请求发到服务端,这种方式要附带参数的话只能放url里了。

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

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

('/upload', function(req, res) {
    var imagedata = '';
    ('binary');
    ('data', function (chunk) {
        imagedata += chunk
    });
    ('end', function (chunk) {
        (filePath, imagedata, 'binary', function(err){
            if (err) throw err
            ({
            ......
            });
        })
    });
});

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

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

参考
  • /photo/
  • /articles/2012/07/28/exif-orientation-handling-is-a-ghetto/
  • /questions/13055915/ios-6-safari-image-upload-orientation-wrong
  • /blog/2012/09/xmlhttprequest_level_2.html
  • /smileEvday/p/
  • /fex-team/webuploader
  • /blog/382528
  • http:///post/node-js-canvas-base64/