?在日常工作中,上传文件是一个非常常见的功能。在某些情况下,我们希望能够限制文件上传的类型,例如限制PNG格式的图像的上传。对于这个问题,我们将考虑通过 input 元素的 accept 属性来限制上传的文件类型:
<input type="file" id="inputFile" accept="image/png" />
虽然这种解决方案可以满足大多数场景,但如果用户将JPEG格式图像的后缀改为 .png ,则可以成功突破这一限制。那么该如何解决这个问题呢??我们可以通过读取文件的二进制数据来识别正确的文件类型。在介绍实际的实现方案之前,我们先来介绍一些相关的知识。
要查看图像对应的二进制数据,可以使用一些编辑器,如Windows平台下的WinHex或macOS平台下的Synalyze It!Pro十六进制编辑器。但是,这里我们使用Visual Studio Code编辑器中的Binary Viewer扩展来查看与个人头像对应的二进制数据。
计算机不是通过图片的后缀名来区分不同的图片类型,而是通过“Magic Number”来区分。对于某些类型的文件,前几个字节的内容是固定的,可以根据这些字节的内容来判断文件的类型。
常见图像类型对应的幻数如下图所示:
接下来,让我们使用二进制查看器扩展来验证我的头像的图像类型是否正确?
从上图可以看出,PNG类型图像的前8个字节为0x89 50 4E 47 0D 0A 1A 0A。当您将 bytefer-avatar.png 文件更改为 bytefer-avatar.jpeg ,并用编辑器打开它查看图像的二进制内容时,您会发现文件的前8个字节保持不变。但是如果你使用input[type= " file "]元素来读取文件信息,将会输出如下结果:
File
lastModified: 1658647747405
lastModifiedDate: Sun Jul 24 2022 15:29:07
name: "bytefer-avatar.jpeg"
size: 47318
type: "image/jpeg"
webkitRelativePath: ""
[[Prototype]]: File
文件扩展名或文件的MIME类型无法识别正确的文件类型。接下来,我们将介绍如何在上传图像时通过读取图像的二进制信息来确保图像类型正确。
在获得文件对象之后,我们可以通过FileReader API读取文件的内容。因为我们不需要读取文件的完整信息,所以我们封装了一个 readBuffer 函数来读取文件中指定范围的二进制数据。
function readBuffer(file, start = 0, end = 2) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.onerror = reject;
reader.readAsArrayBuffer(file.slice(start, end));
});
}
对于PNG类型的图像,文件的前8个字节是0x89 50 4E 47 0D 0A 1A 0A。因此,当我们检测所选择的文件是否是PNG类型的图像时,我们只需要读取前8个字节的数据,并逐一判断每个字节的内容是否一致。
实现逐字节比较和更好的代码重用。让我们来定义一个check函数:
function check(headers) {
return (buffers, options = { offset: 0 }) =>
headers.every(
(header, index) => header === buffers[options.offset + index]
);
}
基于前面定义的readBuffer和check函数,我们可以实现检测PNG图像的功能:
Html代码
<div>
Choose File:<input type="file" id="inputFile" accept="image/*"
onchange="handleChange(event)" />
<p id="realFileType"></p>
</div>
JavaScript代码
const isPNG = check([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const realFileElement = document.querySelector("#realFileType");
async function handleChange(event) {
const file = event.target.files[0];
const buffers = await readBuffer(file, 0, 8);
const uint8Array = new Uint8Array(buffers);
realFileElement.innerText = `The type of ${file.name} is:${
isPNG(uint8Array) ? "image/png" : file.type
}`;
}
运行成功后,对应的检测结果如下图所示:
完整代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Type Detect Demo</title>
</head>
<body>
<div>
<input type="file" id="inputFile" accept="image/*" onchange="handleChange(event)"/>
<p id="realFileType"></p>
</div>
<script>
function check(headers) {
return (buffers,options={
offset: 0
})=>headers.every((header,index)=>header === buffers[options.offset + index]);
}
?
function readBuffer(file, start=0, end=2) {
return new Promise((resolve,reject)=>{
const reader = new FileReader();
reader.onload = ()=>{
resolve(reader.result);
}
;
reader.onerror = reject;
reader.readAsArrayBuffer(file.slice(start, end));
}
);
}
?
const isPNG = check([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const realFileElement = document.querySelector("#realFileType");
?
async function handleChange(event) {
const file = event.target.files[0];
const buffers = await readBuffer(file, 0, 8);
const uint8Array = new Uint8Array(buffers);
realFileElement.innerText = `The type of ${file.name} is:${isPNG(uint8Array) ? "image/png" : file.type}`;
}
</script>
</body>
</html>
如果要检测JPEG文件格式,则需要定义isJPEG函数。
const isJPEG = check([0xff, 0xd8, 0xff]);
但是,如果您想检测其他类型的文件,比如PDF文件,该怎么办呢??这里我们首先使用二进制查看器扩展来查看PDF文件的二进制内容:
从上图可以看出,PDF文件的前4个字节是0x25 50 44 46,对应的字符串是%PDF。为了让用户更直观地识别检测类型,我们可以定义一个stringToBytes函数:
function stringToBytes(string) {
return [...string].map((character) => character.charCodeAt(0));
}
基于stringToBytes函数,我们可以像下面这样轻松地定义isPDF函数:
const isPDF = check(stringToBytes("%PDF"));
isPDF函数用来实现PDF文件检测功能。但在实际工作中,会遇到各种类型的文件。对于这种情况,您可以使用优秀的第三个库来实现文件检测功能,例如文件类型库。
以上就是如何使用JavaScript检测文件类型的内容。在实际工作中,对于文件上传场景,出于安全考虑,建议在开发过程中限制上传文件的类型。对于更严格的场景,您可以考虑使用本文中描述的方法来验证文件类型。
?欢迎关注公众号:文本魔术,了解更多