做者: Cheironjavascript
從網頁調起手機拍照時,不少相機程序會自動根據你拍照的方向旋轉以調整照片顯示,可是上傳的照片倒是原始的方向。因而經常形成拍好的照片在網頁上面上下左右顛倒。html
對此的解決辦法就是,讀取照片 EXIF 信息中的 Orientation 字段,以主動旋轉照片。本文將詳細解讀如何使用javascript讀取EXIF的信息。java
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,以及下面的例子。
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, 也可微信搜索小程序「美菜產品技術團隊」,乾貨滿滿且每週更新,想學習技術的你不要錯過哦。