• 前端上传数据-图片和视频格式校验


    上一篇用 promise 嵌套实现了按 excel 行顺序上传数据,这篇要解决的问题是图片和视频格式校验,图片主要有 jpg png gif 视频 mp4

    由于用户选择的资源可能并不是真正的多媒体文件,使用 js 的 file.type 方法获取的文件类型可能不准确,比如将 .xlsx 改为 .jpg, file.type 得到的类型是image/jpeg

    客户端拉取资源时,图片和视频的分辨率也一并获取,而上传由前端控制,所以上传时对资源要进行比较准确的判断。

    我的判断策略:

    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({ 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({ 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 以便读取字节码,只需要传入偏移量即可

    校验代码

    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)
      })
    }
    

    那么多媒体文件的校验函数

    // 如果传入的类型与实际不符,则不上传,防止图片类型上传视频,或视频类型上传图片
    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)
        }
      })
    },
    
  • 相关阅读:
    (转载)Linux进程基础
    C语言字符串
    DNS域名解析服务
    Linux-SSH远程管理
    Linux文件系统深入了解
    Linux进程和计划任务管理
    Linux账户与权限管理
    MySQL实现读写分离
    SQL数据库常用函数
    MySQL进阶查询(二)
  • 原文地址:https://www.cnblogs.com/wbjxxzx/p/10342156.html
Copyright © 2020-2023  润新知