「ArrayBuffer」應用-以自動調整照片方向為例
從網頁調起手機拍照時,很多相機程式會自動根據你拍照的方向旋轉以調整照片顯示,但是上傳的照片卻是原始的方向。於是常常造成拍好的照片在網頁上面上下左右顛倒。
對此的解決辦法就是,讀取照片 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 的格式

可以看到緊鄰 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) } 複製程式碼
參考:
- ofollow,noindex">Description of Exif file format
- ArrayBuffer
原文連結: tech.meicai.cn/detail/59, 也可微信搜尋小程式「美菜產品技術團隊」,乾貨滿滿且每週更新,想學習技術的你不要錯過哦。