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