// 检测官方文档: 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) }) }