「ArrayBuffer」應用-以自動調整照片方向爲例

做者: Cheironjavascript

背景

從網頁調起手機拍照時,不少相機程序會自動根據你拍照的方向旋轉以調整照片顯示,可是上傳的照片倒是原始的方向。因而經常形成拍好的照片在網頁上面上下左右顛倒。html

對此的解決辦法就是,讀取照片 EXIF 信息中的 Orientation 字段,以主動旋轉照片。本文將詳細解讀如何使用javascript讀取EXIF的信息。java

ArrayBuffer, TypedArray 和 DataView

ArrayBuffer, TypedArray 和 DataView 共同爲 javascript 操做二進制數據提供了便利的途徑。es6

ArrayBuffer 是一塊內存,或者說表明了一段存儲着二進制數據的內容。他不能直接被讀寫,只能經過 TypedArray 或者 DataView 來讀寫。ArrayBuffer 是一個構造函數,接受一個整數做爲參數,即表示分配多少字節的內存。如 const ab = new ArrayBuffer(32) 就分配了一段 16字節的連續內存區域,每一個字節的默認值是0. 同時,一些 javascript API 的返回結果也是 ArrayBuffer, 好比本文將談到的 FileReader API, 它的 readAsArrayBuffer 方法就會返回一個 ArrayBuffer 對象。canvas

TypedArray 是一類構造函數的總稱,包括 Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array 共 9 種。用這九個構造函數生成的 typed array,和數組具備相似的行爲。如都有 length 屬性,均可以經過 [] 訪問元素,也可使用數組大部分的方法。小程序

好比上文建立的 ab 對象。能夠用 const i8view = new Int8Array(ab) 建立一個8位有符號整數的視圖。由於 ab 有 32 個字節,int8 佔一個字節,因此 i8view 的每一項至關於 ab 的一個字節,所以 i8view.length = 32,每一項都是 0.數組

咱們也能夠用 const ui32view = new Uint32Array(ab) 建立一個32位無符號整數的視圖。由於 ab 有 32 個字節,uint32 佔四個字節,因此 ui32view 的每一項至關於 ab 的四個字節,所以 ui32view.length = 8, 由於 ab 的每一個字節都是0, 4個字節一塊兒做爲 Uint32 計算仍是0, 因此,ui32view 的每一項仍然都是 0.微信

能夠看到,在這個過程當中,ab 自己沒有變化,建立不一樣視圖的過程,只是把 ab 的數據做爲 int8, Uint32 或其餘格式的數據來處理而已。async

Typed array 和 array 的區別在於 typed array 的全部成員都是同一類型(也就是 「typed」 的含義),且徹底連續沒有空位。若是傳入數組長度來初始化,那麼因此元素默認值都是 0. TypedArray 只是一種視圖,自己不存儲數據,數據存在 ArrayBuffer 中。TypedArray 適用於處理簡單類型的二進制數據,複雜的就須要 DataView.函數

DataView 能夠定義一個複合視圖。好比 Uint8Array 定義的視圖,因此元素都是 無符號8位整數,而 DataView 定義的視圖,能夠第一個字節是 Uint8, 第二個字節是 Int16 等,且能夠自定義字節序。具體用法能夠參考MDN,以及下面的例子。

JEPG 及 EXIF 的格式

JPEG 文件大致分爲兩個部分:標記碼和壓縮數據。

標記碼由兩個字節組成,前一個是固定值 0xFF,後一個是不一樣意義對應的數值。如 0xFFD8 表示 SOI (Start of Image),0xFFD9 表示 EOI,即 End of Image. 咱們關注的 EXIF 信息與 0xFFE0 0xFFEF 範圍的標記有關。這些區域叫作 應用程序保留區N(ApplicationN),如 0xFFE0 是 App0. 咱們須要的 EXIF 由 App1 標記,便是位於 0xFFE1 到 下一個 0xFFE1 到 下一個 0xFF 標記之間的數據。

EXIF 的格式

能夠看到緊鄰 FFE1 標識的後兩位,是 APP1 的數據大小,位於 TIFF header 以後的是 IFD0 即 Image File Directory. 它包含了圖片信息數據。下面的表格描述了 IFD 的數據格式。

IFD 的格式

TTTT 的 2bytes 數據表示 Tag,ffff 這 2bytes 表示數據的類型。NNNNNNNN 這 4bytes 是組成元素的數量。DDDDDDDD 這 4bytes 是數據自己或數據的偏移量。

在本例中,圖像方向 Orientation 的 Tag Number 是 0x0112;數據類型是 unsigned short, 對應的 ffff 是 0x0003, 組成元素只有一個,因此 NNNNNNNN 是 00000001. DDDDDDDD比較麻煩,有兩種狀況。若是 數據類型 * 組成元素數量 < 4bytes, 那麼,DDDDDDDD 就是改標籤的值,反之則是數據存儲地址的偏移量。Unsigned short 類型的一個組成元素佔 2bytes, 只有一個,因此 2bytes * 1 < 4bytes, 所以對於 Orientation 標籤來講,DDDDDDDD 就是該標籤的值。(有關細節請參考參考文檔中的 1

Orientation 的取值和含義。

通常手機轉一圈拍出來的是 1 6 3 8 四個值。

圖片處理

先使用 FileReader API 把 input 標籤輸入的圖片讀取成 ArrayBuffer

const reader = new FileReader()
reader.onload = async function () {
	const buffer  = reader.result
	const orientation = getOrientation(buffer)
	const image = await rotateImage(buffer, orientation)
}
reader.readAsArrayBuffer(file)
複製代碼

再看 getOrientation 函數的實現。

function getOrientation(buffer) {
	// 創建一個 DataView
	const dv = new DataView(buffer)
	// 設置一個位置指針
	let idx = 0
	// 設置一個默認結果
	let value = 1
	// 檢測是不是 JPEG
	if (buffer.length < 2 || dv.getUint16(idx) !== 0xFFD8 {
		return false
	}
	idx += 2
	let maxBytes = dv.byteLength
	// 遍歷文件內容,找到 APP1, 即 EXIF 所在的標識
	while (idx < maxBytes - 2) {
		const uint16 = dv.getUint16(idx)
		idx += 2
		switch (uint16) {
			case 0xFFE1:
				// 找到 EXIF 後,在 EXIF 數據內遍歷,尋找 Orientation 標識
				const exifLength = dv.getUint16(idx)
				maxBytes = exifLength - 2
				idx += 2
				break
			case 0x0112:
				// 找到 Orientation 標識後,讀取 DDDDDDDD 部分的內容,並把 maxBytes 設爲 0, 結束循環。
				value = dv.getUint16(idx + 6, false)
				maxBytes = 0
				break
		}
	}
	return value
}
複製代碼

在來看 rotateImage 的實現:

function rotateImage (buffer, orientation) {
	// 利用 canvas 來旋轉
	const canvas = document.createElement('canvas')
	const ctx = canvas.getContext('2d')
	// 利用 image 對象來把圖片畫到 canvas 上
	const image = new Image()
	// 根據 arrayBuffer 生成圖片的 base64 url
	const url = arrayBufferToBase64Url(buffer)
	return new Promise((resolve, reject) => {
		image.onload = function () {
			const w = image.naturalWidth
			const h = image.naturalHeight
			switch (orientation) {
				case 8:
					canvas.width = h
					canvas.height = w
					ctx.translate(h / 2, w / 2)
					ctx.rotate(270 * Math.PI / 180)
					ctx.drawImage(image, -w / 2, -h / 2)
					break
				case 3:
					canvas.width = w
					canvas.height = h
					ctx.translate(w / 2, h / 2)
					ctx.rotate(180 * Math.PI / 180)
					ctx.drawImage(image, -w / 2, -h / 2)
					break
				case 6:
					canvas.width = h
					canvas.height = w
					ctx.translate(h / 2, w / 2)
					ctx.rotate(90 * Math.PI / 180)
					ctx.drawImage(image, -w / 2, -h / 2)
					break
				default:
					canvas.width = w
					canvas.height = h
					ctx.drawImage(image, 0, 0)
					break
			}
			// 也可使用其餘 API 導出 canvas
			const data = canvas.toDataURL('image/jpeg', 1)
			resolve(data)
			
		}
		image.src = url	
	})
}
複製代碼

arrayBufferToBase64Url 的實現:

function arrayBufferToBase64 (buffer) {
    let binary = ''
	// 這裏用到了 TypedArray
    const bytes = new Uint8Array(buffer)
    const len = bytes.byteLength
    for (let i = 0; i < len; i++) {
		// fromCharCode 方法從指定的 Unicode 值序列建立字符串
	    binary += String.fromCharCode(bytes[ i ])
    }
    // 使用 btoa 方法從 String 對象建立 base-64 編碼的 ASCII 字符串
    return window.btoa(binary)
}
複製代碼

參考:

  1. Description of Exif file format
  2. ArrayBuffer

原文連接: tech.meicai.cn/detail/59, 也可微信搜索小程序「美菜產品技術團隊」,乾貨滿滿且每週更新,想學習技術的你不要錯過哦。

相關文章
相關標籤/搜索