ArrayBuffer实战-识别文件类型

1,567 阅读4分钟

前言

判断文件类型,有种很简单的方法,就是直接识别文件后缀,如果后缀是什么就是什么文件了。但是这样会有缺点,当文件的后缀名被修改了之后,会把文件识别成另外一种类型,然而文件的内容本身并没有变。

这个时候我们就需要直接读取文件内容,而不是仅通过名字来判断文件的类型。

怎么去做呢?

好在JS本身提供了一个数据结构类型,里面可以二进制形式来储存文件的内容,我们通过读取这个数据结构,就能拿到文件的内容了。

这个数据结构是ArrayBuffer

本篇文章默认读者已经掌握了ArrayBuffer相关概念以及用法,如果没有,建议看我这篇文章

判断思路

通过文件的内容来判断的依据是magic number,magic number是文件中的常量,不随文件内容的变更而变化。每个文件的magic number都是不一样的。所以我们可以通过它来判断文件的类型

  1. 现将文件转成ArrayBuffer格式
  2. magic number通常是文件的开头,或者文件的结尾,我们取出文件开头的几个数据,或者结尾的几个数据
  3. 如果这几个数据符合image/png的magic number,文件类型就是png了;如果符合image/jpeg,文件类型就是jpg了

实操

创建一个html文件,并且其中有读取文件的input标签

<input type="file" id="getFile" />

给标签加上事件

getFile.onchange = (e) => {
    const file = getFile.files[0];
    var reader = new FileReader();
    reader.readAsArrayBuffer(file);
    reader.onload = function () {
        console.log(reader.result);
    };
};

我们获取到了文件实例对象file之后,用FileReader实例去读取它,并将其转成ArrayBuffer类型。在onload回调函数中,可以通过reader的result属性拿到这个ArrayBuffer

下面我们可以开始着手判断,先以image/jpeg文件开始

image/jpeg的magic number格式:文件以" FF D8 "开头,并且以 " FF D9 "结束

reader.onload = function () {
    const type = isJpeg(reader.result);
    console.log(type);
};

function isJpeg(buffer){
    var uint8Array = new Uint8Array(buffer);
    var len = uint8Array.length;
    if (
        numToHex(uint8Array[0]) === "FF" &&
        numToHex(uint8Array[1]) === "D8" &&
        numToHex(uint8Array[len - 2]) === "FF" &&
        numToHex(uint8Array[len - 1]) === "D9"
    ) {
        return "jpeg";
    }
    return null;
}

//将十进制的number类型数据转成十六进制表示的字符串
function numToHex(number) {
    let result = number.toString(16);
    return result.padStart(2, "0").toUpperCase();
}
  • 不能直接读取ArrayBuffer中的内容,必须通过视图,这里用了Uint8Array
  • 我们首先拿了文件开头的两个字节,看是否为" FF D8 ",然后再拿结尾的两个字节,看是否为" FF D9 "
  • 如果匹配成功,就返回" jpeg ";否则返回 null
  • 因为用视图中直接读取ArrayBuffer中的内容,得到的是十进制的数字。比对magic number需要转成十六进制。
  • JS转十六进制需要注意两个地方:
    • 不补零。当十进制10转成十六进制,会得到a,但我们需要 0a。也就是比对magic number的时候,是以一个字节作为基本单位的,所以补零是必要的。例子中用的补零方法是原生的String.padStart()。
    • 大小写。JS中十六进制是以小写格式存在的,在和magic number比对的时候,需要注意大小写

再来判断image/png文件

image/png的magic number格式: 文件以" 89 50 4E 47 0D 0A 1A 0A "开头

reader.onload = function () {
    const type = isPng(reader.result);
    console.log(type);
};

function isPng(buffer){
    var uint8Array = new Uint8Array(buffer);
    var bufferLen = uint8Array.length;
    const magic = ["89", "50", "4E", "47", "0D", "0A", "1A", "0A"];
    var magicLen = magic.length;
    var isMatch = true;
    for (var i = 0; i < magicLen; i++) {
        if (magic[i] !== numToHex(uint8Array[i])) {
            isMatch = false;
            break;
        }
    }
    if (isMatch) {
        return "png";
    }
}

匹配逻辑简单,就不解释了

整理下代码

我们已经完成了jpg和png两种文件的类型判断,我们可以通过相同的方式来判断其他类型的文件。这个就交给同学们自己去完成了

上面的代码写得不太好,所以我把代码整理了一下。下面是完整版的代码

getFile.onchange = function(){
    // console.log(getFile.files);
    const file = getFile.files[0];
    var reader = new FileReader();
    reader.readAsArrayBuffer(file);
    reader.onload = function () {
        var type = judgeType(reader.result);
        console.log(type);
    };
};

// 判断文件类型
function judgeType(buffer) {
    var keys = Object.keys(getType);
    for (var i = 0; i < keys.length; i++) {
        var type = getType[keys[i]](buffer);
        if (type) {
            return type;
        }
    }
    return null;
}

var getType = {
    jpeg: function (buffer) {
        var uint8Array = new Uint8Array(buffer);
        var len = uint8Array.length;
        if (
            numToHex(uint8Array[0]) === "FF" &&
            numToHex(uint8Array[1]) === "D8" &&
            numToHex(uint8Array[len - 2]) === "FF" &&
            numToHex(uint8Array[len - 1]) === "D9"
        ) {
            return "jpeg";
        }
        return null;
    },
    png: function (buffer) {
        var uint8Array = new Uint8Array(buffer);
        var bufferLen = uint8Array.length;
        var magic = ["89", "50", "4E", "47", "0D", "0A", "1A", "0A"];
        var magicLen = magic.length;
        var isMatch = true;
        for (var i = 0; i < magicLen; i++) {
            if (magic[i] !== numToHex(uint8Array[i])) {
                isMatch = false;
                break;
            }
        }
        if (isMatch) {
            return "png";
        }
        return null;
    },
};

function numToHex(number) {
    let result = number.toString(16);
    return result.padStart(2, "0").toUpperCase();
}
  • 将判断类型的函数放在了对象里面
  • 增加了一个judgeType,作用是轮询匹配文件类型,如果匹配成功就返回类型字符串

视频版B站链接

本篇的实战例子是来自B站up主【狗胖爱棉花】,这是链接

视频中有对上面例子更生动丰富的讲解

总结:

  1. 通过后缀名判断文件类型不靠谱,需要用magic number来判断
  2. JS提供了ArrayBuffer,让我们可以读取二进制格式的文件内容
  3. 文中有写的不明白的地方,评论告诉我