tj
2025-06-05 bba272999cc546f65781bf3d20245a3f819af67f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
// 检测官方文档: 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)
  })
}