fix: image file type detect (#24)

This commit is contained in:
MaxtuneLee
2025-06-25 18:49:35 +08:00
committed by GitHub
parent 3302b2d0f6
commit b215871056
5 changed files with 170 additions and 23 deletions

View File

@@ -0,0 +1,56 @@
<!-- Generated by SpecStory -->
# 这里图片的blob type 是 application/octet-stream但是实际上是一张 (2025-06-25 18:15:21)
_**User**_
这里图片的blob type 是 application/octet-stream但是实际上是一张图片请你完善图片判断方法
---
_**Assistant**_
我看到问题了。当前的图片验证方法只依赖于 `blob.type` (MIME type),但有些情况下服务器可能返回 `application/octet-stream` 这样的通用类型,即使文件实际上是图片。我需要完善这个验证方法,增加基于文件头(魔数)的检测。
让我查看一下相关的代码和依赖项: 现在我来完善图片验证方法。我将添加基于文件头(魔数)的检测,以便在 MIME type 不可靠时能够正确识别图片格式:
```
```
现在我需要更新调用这个方法的地方,因为它现在是异步的:
```
```
现在让我检查一下代码是否有任何语法错误: 完美!我已经成功完善了图片判断方法。主要改进包括:
## 🔧 改进内容
1. **异步文件头检测**:将 `isValidImageBlob` 方法改为异步,增加了基于文件头(魔数)的检测能力
2. **多重验证策略**
- 首先检查 MIME 类型,如果有效则直接通过
- 如果 MIME 类型无效或为通用类型(如 `application/octet-stream`),则使用文件头检测
- 支持检测多种图片格式的文件签名
3. **支持的格式检测**
- **JPEG**: `FF D8 FF`
- **PNG**: `89 50 4E 47 0D 0A 1A 0A`
- **WebP**: `52 49 46 46 ... 57 45 42 50`
- **BMP**: `42 4D`
- **TIFF** (两种字节序): `49 49 2A 00` / `4D 4D 00 2A`
- **HEIC/HEIF**: 检查 `ftyp` box 和相关 brand
4. **错误处理**:增加了更详细的日志记录,帮助调试问题
## 🚀 工作原理
当服务器返回 `application/octet-stream` 这样的通用 MIME 类型时,代码现在会:
1. 读取文件的前 12 个字节
2. 检查这些字节是否匹配已知图片格式的文件头
3. 如果匹配,则认为是有效图片并继续处理
4. 如果不匹配,则拒绝处理并报错
这样即使服务器没有正确设置 Content-Type只要文件本身是有效的图片格式系统也能正确识别和处理。
---

View File

@@ -0,0 +1,16 @@
<!-- Generated by SpecStory -->
# 调用 file-type 包完善 #sym:isValidImageBlob 方法,不要硬编码,应该 (2025-06-25 18:22:37)
_**User**_
调用 file-type 包完善 #sym:isValidImageBlob 方法不要硬编码应该用magic number判断文件类型。这个包的文档在https://github.com/sindresorhus/file-type 请参考在浏览器的用法
---
_**Assistant**_
我来帮你使用 `file-type` 包来完善 `isValidImageBlob` 方法,通过 magic number 来判断文件类型而不是硬编码。首先让我查看相关文档。
---

View File

@@ -41,6 +41,7 @@
"consola": "3.4.2",
"dotenv": "16.5.0",
"es-toolkit": "1.39.3",
"file-type": "^21.0.0",
"foxact": "0.2.49",
"heic-to": "1.1.14",
"i18next": "25.2.1",

View File

@@ -1,3 +1,5 @@
import { fileTypeFromBlob } from 'file-type'
import { i18nAtom } from '~/i18n'
import { jotaiStore } from '~/lib/jotai'
import { LRUCache } from '~/lib/lru-cache'
@@ -69,33 +71,40 @@ export class ImageLoaderManager {
/**
* 验证 Blob 是否为有效的图片格式
* 使用 magic number 检测文件类型,而不是依赖 MIME 类型
*/
private isValidImageBlob(blob: Blob): boolean {
// 检查 MIME 类型
const validImageTypes = [
'image/jpeg',
'image/jpg',
'image/png',
'image/webp',
'image/bmp',
'image/tiff',
'image/heic',
'image/heif',
]
// 检查 Content-Type
if (!blob.type || !validImageTypes.includes(blob.type.toLowerCase())) {
console.warn(`Invalid image MIME type: ${blob.type}`)
return false
}
private async isValidImageBlob(blob: Blob): Promise<boolean> {
// 检查文件大小(至少应该有一些字节)
if (blob.size === 0) {
console.warn('Empty blob detected')
return false
}
return true
try {
// 使用 magic number 检测文件类型
const fileType = await fileTypeFromBlob(blob)
if (!fileType) {
console.warn('Could not detect file type from blob')
return false
}
// 检查是否为图片格式
const isValidImage = fileType.mime.startsWith('image/')
if (!isValidImage) {
console.warn(
`Invalid file type detected: ${fileType.ext} (${fileType.mime})`,
)
return false
}
console.info(`Valid image detected: ${fileType.ext} (${fileType.mime})`)
return true
} catch (error) {
console.error('Failed to detect file type:', error)
return false
}
}
async loadImage(
@@ -120,7 +129,7 @@ export class ImageLoaderManager {
try {
// 验证响应是否为图片
const blob = xhr.response as Blob
if (!this.isValidImageBlob(blob)) {
if (!(await this.isValidImageBlob(blob))) {
onLoadingStateUpdate?.({
isVisible: false,
})

69
pnpm-lock.yaml generated
View File

@@ -187,6 +187,9 @@ importers:
es-toolkit:
specifier: 1.39.3
version: 1.39.3
file-type:
specifier: ^21.0.0
version: 21.0.0
foxact:
specifier: 0.2.49
version: 0.2.49(react@19.1.0)
@@ -1740,7 +1743,7 @@ packages:
resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==}
peerDependencies:
'@types/react': '*'
react: 18.3.1
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
@@ -2633,6 +2636,13 @@ packages:
'@tanstack/virtual-core@3.13.9':
resolution: {integrity: sha512-3jztt0jpaoJO5TARe2WIHC1UQC3VMLAFUW5mmMo0yrkwtDB2AQP0+sh10BVUpWrnvHjSLvzFizydtEGLCJKFoQ==}
'@tokenizer/inflate@0.2.7':
resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==}
engines: {node: '>=18'}
'@tokenizer/token@0.3.0':
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
'@tootallnate/once@2.0.0':
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
engines: {node: '>= 10'}
@@ -4363,6 +4373,9 @@ packages:
picomatch:
optional: true
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
figures@6.1.0:
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
engines: {node: '>=18'}
@@ -4371,6 +4384,10 @@ packages:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
file-type@21.0.0:
resolution: {integrity: sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==}
engines: {node: '>=20'}
file-uri-to-path@1.0.0:
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
@@ -4631,6 +4648,9 @@ packages:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -5338,7 +5358,6 @@ packages:
path-match@1.2.4:
resolution: {integrity: sha512-UWlehEdqu36jmh4h5CWJ7tARp1OEVKGHKm6+dg9qMq5RKUTV5WJrGgaZ3dN2m7WFAXDbjlHzvJvL/IUpy84Ktw==}
deprecated: This package is archived and no longer maintained. For support, visit https://github.com/expressjs/express/discussions
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
@@ -6024,6 +6043,10 @@ packages:
strnum@1.1.2:
resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==}
strtok3@10.3.1:
resolution: {integrity: sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw==}
engines: {node: '>=18'}
styled-jsx@5.1.6:
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
engines: {node: '>= 12.0.0'}
@@ -6153,6 +6176,10 @@ packages:
resolution: {integrity: sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==}
engines: {node: '>=0.6'}
token-types@6.0.0:
resolution: {integrity: sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==}
engines: {node: '>=14.16'}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
@@ -6281,6 +6308,10 @@ packages:
uid-promise@1.0.0:
resolution: {integrity: sha512-R8375j0qwXyIu/7R0tjdF06/sElHqbmdmWC9M2qQHpEVbvE4I5+38KJI7LUUmQMp7NVq4tKHiBMkT0NFM453Ig==}
uint8array-extras@1.4.0:
resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==}
engines: {node: '>=18'}
unconfig@7.3.2:
resolution: {integrity: sha512-nqG5NNL2wFVGZ0NA/aCFw0oJ2pxSf1lwg4Z5ill8wd7K4KX/rQbHlwbh+bjctXL5Ly1xtzHenHGOK0b+lG6JVg==}
@@ -9313,6 +9344,16 @@ snapshots:
'@tanstack/virtual-core@3.13.9': {}
'@tokenizer/inflate@0.2.7':
dependencies:
debug: 4.4.1
fflate: 0.8.2
token-types: 6.0.0
transitivePeerDependencies:
- supports-color
'@tokenizer/token@0.3.0': {}
'@tootallnate/once@2.0.0': {}
'@ts-morph/common@0.11.1':
@@ -11181,6 +11222,8 @@ snapshots:
optionalDependencies:
picomatch: 4.0.2
fflate@0.8.2: {}
figures@6.1.0:
dependencies:
is-unicode-supported: 2.1.0
@@ -11189,6 +11232,15 @@ snapshots:
dependencies:
flat-cache: 4.0.1
file-type@21.0.0:
dependencies:
'@tokenizer/inflate': 0.2.7
strtok3: 10.3.1
token-types: 6.0.0
uint8array-extras: 1.4.0
transitivePeerDependencies:
- supports-color
file-uri-to-path@1.0.0: {}
filelist@1.0.4:
@@ -11459,6 +11511,8 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
ieee754@1.2.1: {}
ignore@5.3.2: {}
ignore@7.0.4: {}
@@ -12780,6 +12834,10 @@ snapshots:
strnum@1.1.2: {}
strtok3@10.3.1:
dependencies:
'@tokenizer/token': 0.3.0
styled-jsx@5.1.6(@babel/core@7.27.1)(react@19.1.0):
dependencies:
client-only: 0.0.1
@@ -12902,6 +12960,11 @@ snapshots:
toidentifier@1.0.0: {}
token-types@6.0.0:
dependencies:
'@tokenizer/token': 0.3.0
ieee754: 1.2.1
tr46@0.0.3: {}
tree-kill@1.2.2: {}
@@ -13011,6 +13074,8 @@ snapshots:
uid-promise@1.0.0: {}
uint8array-extras@1.4.0: {}
unconfig@7.3.2:
dependencies:
'@quansync/fs': 0.1.3