咱們先看看linkface給開放的接口:javascript
字段 | 類型 | 必需 | 描述 |
---|---|---|---|
api_id | string | 是 | API 帳戶 |
api_secret | string | 是 | API 密鑰 |
selfie_file | file | 見下方註釋 | 需上傳的圖片文件 1,上傳本地圖片進行檢測時選取此參數 |
selfie_url | string | 見下方註釋 | 圖片 1 的網絡地址,採用抓取網絡圖片方式時需選取此參數 |
selfie_image_id | file | 見下方註釋 | 圖片 1 的id,在雲端上傳過圖片可採用 |
historical_selfie_file | file | 見下方註釋 | 需上傳的圖片文件 2,上傳本地圖片進行檢測時選取此參數 |
historical_selfie_url | string | 見下方註釋 | 圖片 2 的網絡地址,採用抓取網絡圖片方式時需選取此參數 |
historical_selfie_image_id | string | 見下方註釋 | 圖片 2 的id,在雲端上傳過圖片可採用 |
selfie_auto_rotate | boolean | 否 | 值爲 true 時,對圖片 1 進行自動旋轉。默認值爲 false,不旋轉 |
historical_selfie_auto_rotate | boolean | 否 | 值爲 true 時,對圖片 2 進行自動旋轉。默認值爲 false,不旋轉 |
如文件所示,接口須要同時上傳兩個文件和兩個字段,通常咱們的web前端就很簡單了,兩個file類型的input組成的form提交就能夠,若想實現文件的異步上傳通俗的方式就是安裝瀏覽器安全插件,或者就是使用form表單的提交target指向爲iframe,而後將iframe隱藏,使用視窗的父子級調用完成,可是這仍須要咱們使用form組件選擇文件,很顯然這樣會使得咱們的移動APP體驗極差,咱們指望的就是使用相機拍完照而後直接異步上傳執行檢測,固然咱們可使用XMLHTTPReauest2拼接一個formatdata上傳前端
//不徹底代碼 let formData = new FormData(); formData.append('fileName',input.files[0]); xhr.open("post", encodeURI(url)); xhr.send(formData);
可是,在web端,若是用戶不使用input選擇文件,咱們是沒法私自獲取並上傳文件的,這個瀏覽器的安全機制,想一想若是能夠拼接file://私自獲取文件,咱們還安全麼?java
那麼針對於cordova plugin 就至關於咱們瀏覽器的插件了,道理是必定的,經過js的方式調用底層接口。咱們首先可以想獲得的就是file-transfer這個插件,可是很遺憾的告訴你,這個插件一次只能上傳一個文件, https://github.com/apache/cordova-plugin-file-transfer,git
Parameters: fileURL: Filesystem URL representing the file on the device or a data URI. For backwards compatibility, this can also be the full path of the file on the device. (See Backwards Compatibility Notes below) server: URL of the server to receive the file, as encoded by encodeURI(). successCallback: A callback that is passed a FileUploadResult object. (Function) errorCallback: A callback that executes if an error occurs retrieving the FileUploadResult. Invoked with a FileTransferError object. (Function) options: Optional parameters (Object). Valid keys: fileKey: The name of the form element. Defaults to file. (DOMString) fileName: The file name to use when saving the file on the server. Defaults to image.jpg. (DOMString) httpMethod: The HTTP method to use - either PUT or POST. Defaults to POST. (DOMString) mimeType: The mime type of the data to upload. Defaults to image/jpeg. (DOMString) params: A set of optional key/value pairs to pass in the HTTP request. (Object, key/value - DOMString) chunkedMode: Whether to upload the data in chunked streaming mode. Defaults to true. (Boolean) headers: A map of header name/header values. Use an array to specify more than one value. On iOS, FireOS, and Android, if a header named Content-Type is present, multipart form data will NOT be used. (Object) trustAllHosts: Optional parameter, defaults to false. If set to true, it accepts all security certificates. This is useful since Android rejects self-signed security certificates. Not recommended for production use. Supported on Android and iOS. (boolean)
我真搞不懂既然cordova plugin封裝,爲啥不封裝成文件數組接口呢,支持多文件和困難麼?那麼咱們就來看看他的源碼:github
boolean multipartFormUpload = (headers == null) || !headers.has("Content-Type"); if (multipartFormUpload) { conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY); } // Set the cookies on the response String cookie = getCookies(target); if (cookie != null) { conn.setRequestProperty("Cookie", cookie); } // Handle the other headers if (headers != null) { addHeadersToRequest(conn, headers); } /* * Store the non-file portions of the multipart data as a string, so that we can add it * to the contentSize, since it is part of the body of the HTTP request. */ StringBuilder beforeData = new StringBuilder(); try { for (Iterator<?> iter = params.keys(); iter.hasNext();) { Object key = iter.next(); if(!String.valueOf(key).equals("headers")) { beforeData.append(LINE_START).append(BOUNDARY).append(LINE_END); beforeData.append("Content-Disposition: form-data; name=\"").append(key.toString()).append('"'); beforeData.append(LINE_END).append(LINE_END); beforeData.append(params.getString(key.toString())); beforeData.append(LINE_END); } } } catch (JSONException e) { Log.e(LOG_TAG, e.getMessage(), e); } beforeData.append(LINE_START).append(BOUNDARY).append(LINE_END); beforeData.append("Content-Disposition: form-data; name=\"").append(fileKey).append("\";"); beforeData.append(" filename=\"").append(fileName).append('"').append(LINE_END); beforeData.append("Content-Type: ").append(mimeType).append(LINE_END).append(LINE_END); byte[] beforeDataBytes = beforeData.toString().getBytes("UTF-8"); byte[] tailParamsBytes = (LINE_END + LINE_START + BOUNDARY + LINE_START + LINE_END).getBytes("UTF-8");
看到了嗎,它是拼接了報文,這就是可以解釋它爲啥還須要依賴 cordova-plugin-file這個插件了,它能夠直接獲取文件ArrayBuffer,很聰明啊,真的很聰明,爲何拼報文?豈不是很麻煩,正常我麼使用java的http client是須要依賴 httpclient-4.0.1.jar commons-codec-1.3.jar apache-mime4j-0.6.jar httpcore-4.0.1.jar httpmime-4.0.1.jar ,這無形之中就增大了app的大小,做爲卡插拔式的插件,大小也是一個硬傷,因此封裝插件的同窗們學習吧,人家可不是蓋的,拼接報文天然使得插件不須要依賴那些包了。web
咱們開腦補一下http報文協議:apache
一個HTTP請求報文由請求行(request line)、請求頭部(header)、空行和請求數據4個部分組成,下圖給出了請求報文的通常格式。api
因此按照標準拼寫報文也是能夠的。數組
可是我是一個H5工程師,我首先會使用H5技術去解決這件事,否則我就只能發揮java技能更改file-transfer這個插件了。XHR拼接formdata,能夠是file也能夠是一個blob,我曾將想過是否是有接口可以模擬封裝input的file或者使用FileReader,然而仍是那句話,瀏覽器爲了安全不會讓咱們本身拼接file:// 的,可是cordova跨平臺能夠訪問文件系統(你能夠看一下 https://github.com/apache/cordova-plugin-file裏http-equiv="Content-Security-Policy"相關的描述),畢竟咱們開發的是移動app,這個功能是不可缺乏的,咱們使用cordova的file plugin仍是能夠獲取文件的咱們來看看ionic2提供的接口(http://ionicframework.com/docs/v2/native/file/ ):瀏覽器
readAsArrayBuffer(path, file)
Read file and return data as an ArrayBuffer.
Param | Type | Details |
---|---|---|
path | string |
Base FileSystem. Please refer to the iOS and Android filesystems above |
file | string |
Name of file, relative to path. |
Returns: Promise<ArrayBuffer|FileError>
Returns a Promise that resolves with the contents of the file as ArrayBuffer or rejects with an error.
驚喜吧!有個這個咱們就可以本身拼寫blob類型的formdata了,話很少說咱們直接上代碼:
先寫封裝一個文件轉換類,file-convert-util.ts:
import {File, FileError} from "ionic-native"; /*** * @author 趙俊明 */ export class FileConvertUtil { constructor() { } //講文件轉換爲Blob public static convertFileToBlob(fullFilePath: string): Promise<Blob|FileError> { return new Promise((resolve, reject)=> { FileConvertUtil.convertFileToArrayBuffer(fullFilePath).then((arrayBuffer)=> { resolve(new Blob([arrayBuffer], {type: "image/" + FileConvertUtil.extractFileType(fullFilePath)})); }).catch((reason)=> { reject(reason); }); }); } //將文件裝換爲ArrayBuffer public static convertFileToArrayBuffer(fullFilePath: string): Promise<ArrayBuffer | FileError> { return File.readAsArrayBuffer(FileConvertUtil.extractFilePath(fullFilePath), FileConvertUtil.extractFileName(fullFilePath)); } //截取文件路徑 public static extractFilePath(fullFilePath: string): string { return fullFilePath.substr(0, fullFilePath.lastIndexOf('/')); } //截取文件名稱 public static extractFileName(fullFilePath: string): string { return fullFilePath.substr(fullFilePath.lastIndexOf('/') + 1); } //截取文件類型 public static extractFileType(fullFilePath: string): string { return fullFilePath.split(".")[1]; } }
基於XHR2的upload,xhr-multipart-upload.ts:
import {BrowserXhr} from "@angular/http"; import {FileConvertUtil} from "./file-convert-util"; import {FileError} from "ionic-native"; import {Injectable, Component} from "@angular/core"; /** * @author zhaojunming */ export class XHRMultipartFileUpload { private static browserXhr = new BrowserXhr(); constructor() { } public static upload(url: string, files: {name: string,path: string}[], params: any): Promise<any> { const xhr = XHRMultipartFileUpload.browserXhr.build(); xhr.open("post", encodeURI(url)); let formData = new FormData(); return new Promise((resolve, reject)=> { if (params) { for (let _v in params) { if (params.hasOwnProperty(_v)) { formData.append(_v, params[_v]); } } } let blobPromiseList: Array<Promise<Blob|FileError>> = []; files.forEach((file)=> { blobPromiseList.push(FileConvertUtil.convertFileToBlob(file.path)); }); Promise.all(blobPromiseList).then((result)=> { result.forEach((blob, index)=> { formData.append(files[index].name, blob, FileConvertUtil.extractFileName(files[index].path)); }); xhr.onreadystatechange = ()=> { if (xhr.readyState == 4) { if (xhr.status == 200) { resolve(JSON.parse(xhr.responseText)); } else { reject({code: xhr.status, message: JSON.parse(xhr.responseText)}); } } } xhr.send(formData); }).catch((reason)=> { reject(reason); }); }); } }
調用linkface的provider,linkface-verfication.ts:
/*** * @author 趙俊明 */ import {Injectable, Component} from "@angular/core"; import {XHRMultipartFileUpload} from "./xhr-multipart-upload"; import {Storage, LocalStorage} from "ionic-angular"; //400 錯誤碼對應信息 const ERROR_MAPPING = { "ENCODING_ERROR": "參數非UTF-8編碼", "DOWNLOAD_TIMEOUT": "網絡地址圖片獲取超時", "DOWNLOAD_ERROR": "網絡地址圖片獲取失敗", "IMAGE_FILE_SIZE_TOO_BIG": "圖片體積過大 ", "IMAGE_ID_NOT_EXIST": "圖片不存在", "NO_FACE_DETECTED": "圖片未檢測出人臉 ", "CORRUPT_IMAGE": "文件不是圖片文件或已經損壞", "INVALID_IMAGE_FORMAT_OR_SIZE": "圖片大小或格式不符合要求", "INVALID_ARGUMENT": "請求參數錯誤", "UNAUTHORIZED": "帳號或密鑰錯誤", "KEY_EXPIRED": "帳號過時", "RATE_LIMIT_EXCEEDED": "調用頻率超出限額", "NO_PERMISSION": "無調用權限", "NOT_FOUND": "請求路徑錯誤", "INTERNAL_ERROR": "服務器內部錯誤" }; @Injectable() export class LinkFaceVerfication { //普通照片比對URL private historicalSelfieVerificationURL = "https://v1-auth-api.visioncloudapi.com/identity/historical_selfie_verification"; //公安水印照片與普通照片比對URL private selfieWatermarkVerificationURL = "https://v1-auth-api.visioncloudapi.com/identity/selfie_watermark_verification"; private apiId: string; private apiSecret: string; //LocalStorage private storage = new Storage(LocalStorage); constructor() { this.getApiId() .then(apiId=> { this.apiId = apiId || "6b666502c4324026b8604c8001a2cd14"; }) .catch(()=> { this.apiId = "6b666502c4324026b8604c8001a2cd14"; }); this.getApiSecret() .then(apiSecret=> { this.apiSecret = apiSecret || "28cf8b8693e54d0b930d0a5089831841"; }) .catch(()=> { this.apiSecret = "28cf8b8693e54d0b930d0a5089831841"; }); } //普通照片比對 public historicalSelfieVerification(selfie_file: string, historical_selfie_file: string, selfie_auto_rotate: boolean = true, historical_selfie_auto_rotate: boolean = true): Promise<any> { let params = { api_id: this.apiId, api_secret: this.apiSecret, selfie_auto_rotate: selfie_auto_rotate, historical_selfie_auto_rotate: historical_selfie_auto_rotate }; let files = [] files.push({name: "selfie_file", path: selfie_file}); files.push({name: "historical_selfie_file", path: historical_selfie_file}); return new Promise((resolve, reject)=> { XHRMultipartFileUpload.upload(this.historicalSelfieVerificationURL, files, params) .then(result=> { resolve(result); }) .catch(error=> { if (error && error.code == 400) { reject(ERROR_MAPPING[error.message.status]); } else { reject(JSON.stringify(error)); } }); }); } //公安水印照片與普通照片比對 public selfieWatermarkVerification(selfie_file: string, watermark_picture_file: string): Promise<any> { let params = {api_id: this.apiId, api_secret: this.apiSecret}; let files = [] files.push({name: "selfie_file", path: selfie_file}); files.push({name: "watermark_picture_file", path: watermark_picture_file}); return new Promise((resolve, reject)=> { XHRMultipartFileUpload.upload(this.selfieWatermarkVerificationURL, files, params) .then(result=> { resolve(result); }) .catch(error=> { if (error && error.code == 400) { reject(ERROR_MAPPING[error.message.status]); } else { reject(JSON.stringify(error)); } }); }); } setApiId(apiId): boolean { if (apiId) { this.apiId = apiId; this.storage.set("apiId", apiId); return true; } return false; } setApiSecret(apiSecret): boolean { if (apiSecret) { this.apiSecret = apiSecret; this.storage.set("apiSecret", apiSecret); return true; } return false; } getApiId(): Promise<string> { return this.storage.get("apiId"); } getApiSecret(): Promise<string> { return this.storage.get("apiSecret"); } }
看看咱們怎麼調用:
this.linkFaceVerfication.historicalSelfieVerification(this.selfie_file, this.historical_selfie_file, true, true) .then(result=> { this.confidence = (result.confidence * 100).toFixed(2); this.uploading = false; }) .catch(reason=> { this.toastMessage(reason); this.uploading = false; });
咱們來看看效果: