1. 程式人生 > >移動端Web上傳圖片實踐

移動端Web上傳圖片實踐

原文連結

其他連結:

http://tgideas.qq.com/webplat/info/news_version3/804/808/811/m579/201409/278736.shtml

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,他更輕便,易裝易用。在JavaPHP、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傳到後端是否可行呢,後面試一試)

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/) 就是把原始圖片直接上傳的,這種方式是否適合直接使用或者還有哪些可以優化的地方有待驗證。

參考