上一篇用 promise 嵌套实现了按 excel 行顺序上传数据,这篇要解决的问题是图片和视频格式校验,图片主要有 jpg png gif 视频 mp4
由于用户选择的资源可能并不是真正的多媒体文件,使用 js 的 file.type 方法获取的文件类型可能不准确,比如将 .xlsx 改为 .jpg, file.type 得到的类型是image/jpeg
客户端拉取资源时,图片和视频的分辨率也一并获取,而上传由前端控制,所以上传时对资源要进行比较准确的判断。
我的判断策略:
- 判断文件后缀,若不是 jpg/png/gif/mp4 中的一种,则报错
- 对 jpg/png/gif 的文件,读取二进制头信息,满足任一格式则返回相应格式,否则为非法格式
- 获取图片和视频的分辨率,获取成功则是真的成功,否则还是报错
后缀名校验
// 获取图片的 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)
}
})
},