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

WirelessSprucetec發表於2018-11-26

作者: Cheiron

背景

從網頁調起手機拍照時,很多相機程式會自動根據你拍照的方向旋轉以調整照片顯示,但是上傳的照片卻是原始的方向。於是常常造成拍好的照片在網頁上面上下左右顛倒。

對此的解決辦法就是,讀取照片 EXIF 資訊中的 Orientation 欄位,以主動旋轉照片。本文將詳細解讀如何使用javascript讀取EXIF的資訊。

ArrayBuffer, TypedArray 和 DataView

ArrayBuffer, TypedArray 和 DataView 共同為 javascript 操作二進位制資料提供了便利的途徑。

ArrayBuffer 是一塊記憶體,或者說代表了一段儲存著二進位制資料的內容。他不能直接被讀寫,只能通過 TypedArray 或者 DataView 來讀寫。ArrayBuffer 是一個建構函式,接受一個整數作為引數,即表示分配多少位元組的記憶體。如 const ab = new ArrayBuffer(32) 就分配了一段 16位元組的連續記憶體區域,每個位元組的預設值是0. 同時,一些 javascript API 的返回結果也是 ArrayBuffer, 比如本文將談到的 FileReader API, 它的 readAsArrayBuffer 方法就會返回一個 ArrayBuffer 物件。

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 或其他格式的資料來處理而已。

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 的格式

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

可以看到緊鄰 FFE1 標識的後兩位,是 APP1 的資料大小,位於 TIFF header 之後的是 IFD0 即 Image File Directory. 它包含了圖片資訊資料。下面的表格描述了 IFD 的資料格式。

IFD 的格式

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

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 的取值和含義。

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

一般手機轉一圈拍出來的是 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, 也可微信搜尋小程式「美菜產品技術團隊」,乾貨滿滿且每週更新,想學習技術的你不要錯過哦。

相關文章