// 检测官方文档: https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern
|
/** 类型检测掩码集合 */
|
const patternMask = [
|
{
|
name: 'image/x-icon',
|
mask: [0xff, 0xff, 0xff, 0xff],
|
byte: [0x00, 0x00, 0x01, 0x00],
|
},
|
{
|
name: 'image/x-icon',
|
mask: [0xff, 0xff, 0xff, 0xff],
|
byte: [0x00, 0x00, 0x02, 0x00],
|
},
|
{
|
name: 'image/bmp',
|
mask: [0xff, 0xff],
|
byte: [0x42, 0x4d],
|
},
|
{
|
name: 'image/gif',
|
mask: [0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
|
byte: [0x47, 0x49, 0x46, 0x38, 0x37, 0x61],
|
},
|
{
|
name: 'image/gif',
|
mask: [0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
|
byte: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61],
|
},
|
{
|
name: 'image/webp',
|
mask: [0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
|
byte: [0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50],
|
},
|
{
|
name: 'image/png',
|
mask: [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
|
byte: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
|
},
|
{
|
name: 'image/jpeg',
|
mask: [0xff, 0xff, 0xff],
|
byte: [0xff, 0xd8, 0xff],
|
},
|
]
|
|
/** 判断是否是空对象 */
|
export function isEmptyObj(data) {
|
if (!data) return true
|
return JSON.stringify(data) === '{}'
|
}
|
|
/** 生成随机字符串 */
|
export function createId() {
|
return Math.random()
|
.toString(36)
|
.substring(2)
|
}
|
|
/**
|
* 检测是否是动图
|
* 主要针对 Gif 和 Webp 两种格式
|
*/
|
export async function checkIsAnimated({ file, fileUrl, fileType }) {
|
// 参数验证
|
if (!file || !(file instanceof File)) {
|
console.error('isAnimated param check fail: param expected to be File object')
|
return false
|
}
|
// 如果不是 gif 和 webp, 默认作为非动图
|
if (fileType !== 'image/webp' && fileType !== 'image/gif') {
|
return false
|
}
|
|
if (fileType === 'image/webp') {
|
return new Promise(resolve => {
|
const request = new XMLHttpRequest()
|
request.open('GET', fileUrl, true)
|
request.addEventListener('load', () => {
|
resolve(request.response.indexOf('ANMF') !== -1)
|
})
|
request.send()
|
})
|
}
|
if (fileType === 'image/gif') {
|
return new Promise(resolve => {
|
const request = new XMLHttpRequest()
|
request.open('GET', fileUrl, true)
|
request.responseType = 'arraybuffer'
|
request.addEventListener('load', () => {
|
const arr = new Uint8Array(request.response)
|
// make sure it's a gif (GIF8)
|
if (arr[0] !== 0x47 || arr[1] !== 0x49 || arr[2] !== 0x46 || arr[3] !== 0x38) {
|
resolve(false)
|
return
|
}
|
|
// ported from php http://www.php.net/manual/en/function.imagecreatefromgif.php#104473
|
// an animated gif contains multiple "frames", with each frame having a
|
// header made up of:
|
// * a static 4-byte sequence (\x00\x21\xF9\x04)
|
// * 4 variable bytes
|
// * a static 2-byte sequence (\x00\x2C) (some variants may use \x00\x21 ?)
|
// We read through the file til we reach the end of the file, or we've found
|
// at least 2 frame headers
|
let frames = 0
|
for (let i = 0, len = arr.length - 9; i < len && frames < 2; ++i) {
|
if (
|
arr[i] === 0x00
|
&& arr[i + 1] === 0x21
|
&& arr[i + 2] === 0xf9
|
&& arr[i + 3] === 0x04
|
&& arr[i + 8] === 0x00
|
&& (arr[i + 9] === 0x2c || arr[i + 9] === 0x21)
|
) {
|
frames++
|
}
|
}
|
|
// if frame count > 1, it's animated
|
resolve(frames > 1)
|
})
|
request.send()
|
})
|
}
|
}
|
|
/**
|
* 检测文件类型
|
* 使用文件编码进行检测
|
* 支持模式参看: patternMask 定义
|
*/
|
export async function getFileType(file) {
|
if (!(file instanceof File)) {
|
return 'unknown'
|
}
|
return new Promise(resolve => {
|
const fileReader = new FileReader()
|
fileReader.onloadend = e => {
|
const header = new Uint8Array(e.target.result).slice(0, 20)
|
let type = 'unknown'
|
|
// eslint-disable-next-line arrow-body-style
|
const index = patternMask.findIndex(item => {
|
// eslint-disable-next-line arrow-body-style
|
return item.mask.every((subItem, subI) => {
|
// subItem 掩码标志
|
// item.byte[subI] 规范值
|
// header[subI] 文件实际值
|
// eslint-disable-next-line
|
return (subItem & (header[subI] ^ item.byte[subI])) === 0
|
})
|
})
|
|
if (index >= 0) {
|
type = patternMask[index].name
|
}
|
|
resolve(type)
|
}
|
fileReader.readAsArrayBuffer(file)
|
})
|
}
|