前端上傳數據-圖片和視頻格式校驗

上一篇用 promise 嵌套實現了按 excel 行順序上傳數據,這篇要解決的問題是圖片和視頻格式校驗,圖片主要有 jpg png gif 視頻 mp4前端

因爲用戶選擇的資源可能並非真正的多媒體文件,使用 js 的 file.type 方法獲取的文件類型可能不許確,好比將 .xlsx 改成 .jpg, file.type 獲得的類型是image/jpeg數組

客戶端拉取資源時,圖片和視頻的分辨率也一併獲取,而上傳由前端控制,因此上傳時對資源要進行比較準確的判斷。promise

個人判斷策略:ide

  1. 判斷文件後綴,若不是 jpg/png/gif/mp4 中的一種,則報錯
  2. 對 jpg/png/gif 的文件,讀取二進制頭信息,知足任一格式則返回相應格式,不然爲非法格式
  3. 獲取圖片和視頻的分辨率,獲取成功則是真的成功,不然仍是報錯

後綴名校驗

// 獲取圖片的 width height
getImgSize(file) {
  const imgFileType = ['image/jpeg', 'image/png', 'image/gif']
  const filetype = file.type
  const suffix = filetype.substring(filetype.lastIndexOf('/')+1)
  // 返回一個 promise 
  return new Promise((resolve, reject) => {
    let reader = new FileReader()
    reader.onload = function(e){
      const data = e.target.result
      const img = new Image()
      img.onload = function(){
        resolve({width: img.width, height: img.height, ext: suffix })
      }
      img.onerror = function(){
        reject(`[${file.name}]解析失敗,可能圖片格式不正確`)
      }
      img.src = data
    }
    reader.readAsDataURL(file)
  })
},
// 獲取視頻的 width height
getVideoSize(file) {
  const videoType = ['video/mp4',]
  const filetype = file.type
  const suffix = filetype.substring(filetype.lastIndexOf('/')+1)
  // 返回一個 promise 
    return new Promise((resolve, reject) => {
      const url = window.URL.createObjectURL(file)
      const video = document.createElement('video')
      video.onloadedmetadata = evt => {
        // Revoke when you don't need the url any more to release any reference
        window.URL.revokeObjectURL(url)
        resolve({width: video.videoWidth, height: video.videoHeight, ext: suffix })
      }
      video.onerror = evt => {
        reject(`[${file.name}]解析失敗,可能視頻文件格式不正確`)
      }
      video.src = url
      video.load()
  })
},

二進制頭信息

依據 ISO 標準, jpg 文件的前2個字節爲 0xFF, 0xD8
png 前8個字節 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A
gif 有"GIF87a" 和 "GIF89a",依據前6個字節判斷函數

  • GIF87a 0x47, 0x49, 0x46, 0x38, 0x37, 0x61
  • GIF89a 0x47, 0x49, 0x46, 0x38, 0x39, 0x61

js 提供了 getUint8 以便讀取字節碼,只須要傳入偏移量便可this

校驗代碼

const JPEG_SOI = [0xFF, 0xD8]
const JPEG_EOI = [0xFF, 0xD9]

// png的文件頭就是png圖片的前8個字節,其值爲[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
const PNG_HEADER = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]

// GIF files start with a fixed-length header ("GIF87a" or "GIF89a") giving the version
const GIF89A_HEADER = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]
const GIF87A_HEADER = [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]

// 是否小端序
const isLittleEndian = (function() {
  var buffer = new ArrayBuffer(2);
  new DataView(buffer).setInt16(0, 256, true)
  return new Int16Array(buffer)[0] === 256
})()

// byte數組元素是否相等
function isArrayEqual(a, b){
  for(let i=0; i<a.length; i++){
    if(a[i] !== b[i]){
      return false
    }
  }
  return true
}

export function getImageTypeByHeadContent(file){
  // file 其實是一個 Blob 對象
  // 讀取 Blob 對象的前8個字節
  const fileHeader = file.slice(0, 8)
  return new Promise((resolve, reject) => {
    let reader = new FileReader()
    reader.onload = function(e){
      const data = e.target.result
      const header = new DataView(data)
      let bytesArr = []
      for(let i=0; i<header.byteLength; i++){
        bytesArr.push(header.getUint8(i, isLittleEndian))
      }
      if(isArrayEqual(JPEG_SOI, bytesArr.slice(0,2))){
        resolve('jpg')
      }else if(isArrayEqual(PNG_HEADER, bytesArr)){
        resolve('png')
      }else if(isArrayEqual(GIF89A_HEADER, bytesArr.slice(0,6)) || 
               isArrayEqual(GIF87A_HEADER, bytesArr.slice(0,6))
              ){
        resolve('gif')
      }else{
        reject()
      }
    }
    reader.readAsArrayBuffer(fileHeader)
  })
}

那麼多媒體文件的校驗函數url

// 若是傳入的類型與實際不符,則不上傳,防止圖片類型上傳視頻,或視頻類型上傳圖片
getMediaSize(file, validtype){
  const _this = this
  return new Promise((resolve, reject) => {
    if(! _this.hasGotSizeObj.hasOwnProperty(file.name)){
      if(file.type.startsWith('image') && validtype === 'image'){
        _this.getImgSize(file)
        .then(data => {
          // 從文件頭信息沒法識別圖片類型時,之後綴名爲圖片類型
          getImageTypeByHeadContent(file)
          .then(type => {
            data.ext = type
            _this.hasGotSizeObj[file.name] = {extra: data}
            resolve(data)
          })
          .catch(()=>{
            _this.hasGotSizeObj[file.name] = {extra: data}
            resolve(data)
          })
        })
        .catch(err => {
          reject(err)
        })
      }else if(file.type.startsWith('video') && validtype === 'video'){
        _this.getVideoSize(file)
        .then(data => {
          _this.hasGotSizeObj[file.name] = {extra: data}
          resolve(data)
        }).catch(err => {
          reject(err)
        })
      }else{
        reject(`不容許的文件類型: ${file.type}`)
      }
    }else{
      resolve(_this.hasGotSizeObj[file.name].extra)
    }
  })
},
相關文章
相關標籤/搜索