HTML input type="file" event doesn't detect ttf, otf, or js, how do I change that? I would like to have mime types working from the start.
Currently (2024) mime-types are not correctly detected by most browsers when loading font files via a <input type="file">
element.
Where image files are usually detected correctly the returned type is empty for font files such as .woff2
, .woff
, .ttf
,.otf
.
Javascript files are actually detected – albeit with slightly different mime-type outputs in Firefox application/x-javascript
, in chromium/blink-based text/javascript
.
inputFile.addEventListener("input", async(e) => {
let file = e.currentTarget.files[0];
let type = file.type;
mimetype.textContent=!type ? 'none' : type
})
<h1>Show file mime-type</h1>
<p><input id="inputFile" type="file"></p>
<p id="mimetype"></p>
You can detect a mime type based on the first four bytes in a file header.
The bytes where looking for can be found here "6.3. Matching a font type pattern"
The mimetype helper would look something like this
/**
* based on: https://stackoverflow.com/questions/18299806/how-to-check-file-mime-type-with-javascript-before-upload/29672957#29672957
* Add more from http://en.wikipedia.org/wiki/List_of_file_signatures
* or https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern
*/
function getMimeTypeFromHeader(headerString) {
switch (headerString) {
case "774f4632":
type = "font/woff2";
break;
case "774f4646":
type = "font/woff";
break;
case "0100":
type = "font/ttf";
break;
case "4f54544f":
type = "font/otf";
break;
case "89504e47":
type = "image/png";
break;
case "47494638":
type = "image/gif";
break;
case "ffd8ffe0":
case "ffd8ffe1":
case "ffd8ffe2":
type = "image/jpeg";
break;
default:
type = "unknown";
break;
}
return type;
}
As commented by Kaiido we can simplify the header extraction by reading only the first four bytes from the file object like so
let buffer = await file.slice(0, 4).arrayBuffer();
let header = Array.from(new Uint8Array(buffer))
.map((val) => {
return val.toString(16);
})
.join("");
inputFile.addEventListener("input", async(e) => {
let file = e.currentTarget.files[0];
let type = file.type;
let dataURLPrefix = `data:${type},`;
let dataUrl;
/**
* no header in file object - check first bytes in file header
*/
dataUrl = await blobToBase64(file);
if (!type) {
let buffer = await file.slice(0, 4).arrayBuffer();
let header = Array.from(new Uint8Array(buffer))
.map((val) => {
return val.toString(16);
})
.join("");
type = getMimeTypeFromHeader(header);
//prepend correct mime type
let dataURLNoMime = dataUrl.split('base64,')[1]
dataUrl = `data:${type};base64,` + dataURLNoMime
}
dataUrlOut.value = dataUrl;
});
/**
* fetched blob to base64
*/
function blobToBase64(blob) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
}
/**
* based on: https://stackoverflow.com/questions/18299806/how-to-check-file-mime-type-with-javascript-before-upload/29672957#29672957
* Add more from http://en.wikipedia.org/wiki/List_of_file_signatures
* or https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern
*/
function getMimeTypeFromHeader(headerString) {
switch (headerString) {
case "774f4632":
type = "font/woff2";
break;
case "774f4646":
type = "font/woff";
break;
case "0100":
type = "font/ttf";
break;
case "4f54544f":
type = "font/otf";
break;
case "89504e47":
type = "image/png";
break;
case "47494638":
type = "image/gif";
break;
case "ffd8ffe0":
case "ffd8ffe1":
case "ffd8ffe2":
type = "image/jpeg";
break;
default:
type = "unknown";
break;
}
return type;
}
body {
font-family: 'Fira Sans', 'Open Sans', 'Segoe UI', sans-serif;
font-weight: 400;
margin: 0.3em;
}
* {
box-sizing: border-box;
}
textarea {
width: 100%;
min-height: 10em;
resize: vertical
}
img {
max-width: 100%
}
<h1>Font file to dataURL simple</h1>
<p><input id="inputFile" type="file"></p>
<textarea id="dataUrlOut"></textarea>
As most mime-type lookups (e.g mimesniff.spec or wikipedia) use a padded notation we may also normalize the bytestrings to use
for instance 00 01 00 00
instead of 0100
as (ttf) pattern.
Example: normalized pattern
const mimeFromHeader = true;
const mimeLookup = [
//fonts
{ pattern: "00 01 00 00", type: "font/ttf" },
{ pattern: "4F 54 54 4F", type: "font/otf" },
{ pattern: "77 4F 46 46", type: "font/woff" },
{ pattern: "77 4F 46 32", type: "font/woff2" },
//images
{ pattern: "FF D8 FF", type: "image/jpeg" },
{ pattern: "89 50 4E 47 0D 0A 1A 0A", type: "image/png" },
{ pattern: "52 49 46 46 00 00 00 00 57 45 42 50 56 50", type: "image/webp" },
{ pattern: "47 49 46 38 37 61", type: "image/gif" },
{ pattern: "47 49 46 38 39 61", type: "image/gif" },
{ pattern: "42 4D", type: "image/bmp" },
//audio/video
{ pattern: "49 44 33", type: "audio/mpeg" },
{ pattern: "4F 67 67 53 00", type: "application/ogg" },
{ pattern: "52 49 46 46 00 00 00 00 57 41 56 45", type: "audio/wave" },
{ pattern: "1a 45 df a3", type: "video/x-matroska" },
{ pattern: "66 74 79 70 69 73 6F 6D", type: "video/mp4" },
{ pattern: "66 74 79 70 4D 53 4E 56", type: "video/mp4" },
{ pattern: "00 00 00 20 66 74 79 70 4D 53 4E 56", type: "video/mp4" },
{ pattern: "00 00 00 20 66 74 79 70 6d", type: "video/mp4" },
];
inputFile.addEventListener("input", async (e) => {
let file = e.currentTarget.files[0];
let type = file.type;
let dataURLPrefix = `data:${type},`;
let dataUrl;
/**
* no header in file object - check first bytes in file header
*/
dataUrl = await blobToBase64(file);
if (!type || mimeFromHeader) {
// get array buffer from first 4 bytes
let firstBytes = await file.slice(0, 14).arrayBuffer();
// stringify to hexadecimal
let firstBytesString = Array.from(new Uint8Array(firstBytes))
.map((val) => {
val = val.toString(16).padStart(2, '0')
return val
})
.join("");
type = getMimeTypeFromHeader(mimeLookup, firstBytesString);
//replace generic application type with correct mime type
let dataURLNoMime = dataUrl.split("base64,")[1];
dataUrl = `data:${type};base64,` + dataURLNoMime;
}
dataUrlOut.value = dataUrl;
});
/**
* based on: https://stackoverflow.com/questions/18299806/how-to-check-file-mime-type-with-javascript-before-upload/29672957#29672957
* Add more from http://en.wikipedia.org/wiki/List_of_file_signatures
* or https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern
*/
function getMimeTypeFromHeader(mimeLookup, headerString) {
/**
* helper to normalize to longhand/padded first byte notations
*/
const firstByteStringify = (str) => {
let strArr = str.split(" ");
let byteString;
if (strArr.length > 1) {
byteString = str
.split(" ")
.map((val) => {
return val.toString(16).padStart(2, '0').toLowerCase()
})
.join("");
} else {
byteString = str.toLowerCase();
}
return byteString;
};
let mimeType = "";
for (let i = 0; i < mimeLookup.length && !mimeType; i++) {
let mime = mimeLookup[i];
let pattern = firstByteStringify(mime.pattern);
let lenMin = Math.min(pattern.length, headerString.length)
// has riff - check last bytes:
let riff= headerString.substring(0, 8);
let hasRiff = riff==='52494646'
// adjust pattern and header length if shorter than 4 bytes or has riff
pattern = hasRiff ? pattern.substring(pattern.length -12) : pattern.substring(0, lenMin);
let headerShort = hasRiff ? headerString.substring(headerString.length -12) : headerString.substring(0, lenMin);
if (headerShort === pattern) {
mimeType = mime.type;
}
}
return mimeType;
}
/**
* fetched blob to base64
*/
function blobToBase64(blob) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
}
body {
font-family: 'Fira Sans', 'Open Sans', 'Segoe UI', sans-serif;
font-weight: 400;
margin: 0.3em;
}
* {
box-sizing: border-box;
}
textarea {
width: 100%;
min-height: 10em;
resize: vertical
}
img {
max-width: 100%
}
<h1>Font file to dataURL simple</h1>
<p><input id="inputFile" type="file"></p>
<textarea id="dataUrlOut"></textarea>
See also this answer "How to check file MIME type with JavaScript before upload?"