Search code examples
javascripthtmleventsfile-type

HTML input type="file" doesn't detect ttf, otf, or js, how do I change that?


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.


Solution

  • 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?"