How to check file MIME type with javascript before upload?

前端 未结 9 2040
挽巷
挽巷 2020-11-22 03:50

I have read this and this questions which seems to suggest that the file MIME type could be checked using javascript on client side. Now, I understand that the real validati

相关标签:
9条回答
  • 2020-11-22 04:39

    As stated in other answers, you can check the mime type by checking the signature of the file in the first bytes of the file.

    But what other answers are doing is loading the entire file in memory in order to check the signature, which is very wasteful and could easily freeze your browser if you select a big file by accident or not.

    /**
     * Load the mime type based on the signature of the first bytes of the file
     * @param  {File}   file        A instance of File
     * @param  {Function} callback  Callback with the result
     * @author Victor www.vitim.us
     * @date   2017-03-23
     */
    function loadMime(file, callback) {
        
        //List of known mimes
        var mimes = [
            {
                mime: 'image/jpeg',
                pattern: [0xFF, 0xD8, 0xFF],
                mask: [0xFF, 0xFF, 0xFF],
            },
            {
                mime: 'image/png',
                pattern: [0x89, 0x50, 0x4E, 0x47],
                mask: [0xFF, 0xFF, 0xFF, 0xFF],
            }
            // you can expand this list @see https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern
        ];
    
        function check(bytes, mime) {
            for (var i = 0, l = mime.mask.length; i < l; ++i) {
                if ((bytes[i] & mime.mask[i]) - mime.pattern[i] !== 0) {
                    return false;
                }
            }
            return true;
        }
    
        var blob = file.slice(0, 4); //read the first 4 bytes of the file
    
        var reader = new FileReader();
        reader.onloadend = function(e) {
            if (e.target.readyState === FileReader.DONE) {
                var bytes = new Uint8Array(e.target.result);
    
                for (var i=0, l = mimes.length; i<l; ++i) {
                    if (check(bytes, mimes[i])) return callback("Mime: " + mimes[i].mime + " <br> Browser:" + file.type);
                }
    
                return callback("Mime: unknown <br> Browser:" + file.type);
            }
        };
        reader.readAsArrayBuffer(blob);
    }
    
    
    //when selecting a file on the input
    fileInput.onchange = function() {
        loadMime(fileInput.files[0], function(mime) {
    
            //print the output to the screen
            output.innerHTML = mime;
        });
    };
    <input type="file" id="fileInput">
    <div id="output"></div>

    0 讨论(0)
  • 2020-11-22 04:39

    Here is a Typescript implementation that supports webp. This is based on the JavaScript answer by Vitim.us.

    interface Mime {
      mime: string;
      pattern: (number | undefined)[];
    }
    
    // tslint:disable number-literal-format
    // tslint:disable no-magic-numbers
    const imageMimes: Mime[] = [
      {
        mime: 'image/png',
        pattern: [0x89, 0x50, 0x4e, 0x47]
      },
      {
        mime: 'image/jpeg',
        pattern: [0xff, 0xd8, 0xff]
      },
      {
        mime: 'image/gif',
        pattern: [0x47, 0x49, 0x46, 0x38]
      },
      {
        mime: 'image/webp',
        pattern: [0x52, 0x49, 0x46, 0x46, undefined, undefined, undefined, undefined, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50],
      }
      // You can expand this list @see https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern
    ];
    // tslint:enable no-magic-numbers
    // tslint:enable number-literal-format
    
    function isMime(bytes: Uint8Array, mime: Mime): boolean {
      return mime.pattern.every((p, i) => !p || bytes[i] === p);
    }
    
    function validateImageMimeType(file: File, callback: (b: boolean) => void) {
      const numBytesNeeded = Math.max(...imageMimes.map(m => m.pattern.length));
      const blob = file.slice(0, numBytesNeeded); // Read the needed bytes of the file
    
      const fileReader = new FileReader();
    
      fileReader.onloadend = e => {
        if (!e || !fileReader.result) return;
    
        const bytes = new Uint8Array(fileReader.result as ArrayBuffer);
    
        const valid = imageMimes.some(mime => isMime(bytes, mime));
    
        callback(valid);
      };
    
      fileReader.readAsArrayBuffer(blob);
    }
    
    // When selecting a file on the input
    fileInput.onchange = () => {
      const file = fileInput.files && fileInput.files[0];
      if (!file) return;
    
      validateImageMimeType(file, valid => {
        if (!valid) {
          alert('Not a valid image file.');
        }
      });
    };
    

    <input type="file" id="fileInput">

    0 讨论(0)
  • 2020-11-22 04:39

    Short answer is no.

    As you note the browsers derive type from the file extension. Mac preview also seems to run off the extension. I'm assuming its because its faster reading the file name contained in the pointer, rather than looking up and reading the file on disk.

    I made a copy of a jpg renamed with png.

    I was able to consistently get the following from both images in chrome (should work in modern browsers).

    ÿØÿàJFIFÿþ;CREATOR: gd-jpeg v1.0 (using IJG JPEG v62), quality = 90

    Which you could hack out a String.indexOf('jpeg') check for image type.

    Here is a fiddle to explore http://jsfiddle.net/bamboo/jkZ2v/1/

    The ambigious line I forgot to comment in the example

    console.log( /^(.*)$/m.exec(window.atob( image.src.split(',')[1] )) );

    • Splits the base64 encoded img data, leaving on the image
    • Base64 decodes the image
    • Matches only the first line of the image data

    The fiddle code uses base64 decode which wont work in IE9, I did find a nice example using VB script that works in IE http://blog.nihilogic.dk/2008/08/imageinfo-reading-image-metadata-with.html

    The code to load the image was taken from Joel Vardy, who is doing some cool image canvas resizing client side before uploading which may be of interest https://joelvardy.com/writing/javascript-image-upload

    0 讨论(0)
  • 2020-11-22 04:42

    For anyone who's looking to not implement this themselves, Sindresorhus has create a utility that works in the browser and has the header-to-mime mappings for most documents you could want.

    https://github.com/sindresorhus/file-type

    You could combine Vitim.us's suggestion of only reading in the first X bytes to avoid loading everything into memory with using this utility (example in es6):

    import fileType from 'file-type'; // or wherever you load the dependency
    
    const blob = file.slice(0, fileType.minimumBytes);
    
    const reader = new FileReader();
    reader.onloadend = function(e) {
      if (e.target.readyState !== FileReader.DONE) {
        return;
      }
    
      const bytes = new Uint8Array(e.target.result);
      const { ext, mime } = fileType.fromBuffer(bytes);
    
      // ext is the desired extension and mime is the mimetype
    };
    reader.readAsArrayBuffer(blob);
    
    0 讨论(0)
  • 2020-11-22 04:43

    You can easily determine the file MIME type with JavaScript's FileReader before uploading it to a server. I agree that we should prefer server-side checking over client-side, but client-side checking is still possible. I'll show you how and provide a working demo at the bottom.


    Check that your browser supports both File and Blob. All major ones should.

    if (window.FileReader && window.Blob) {
        // All the File APIs are supported.
    } else {
        // File and Blob are not supported
    }
    

    Step 1:

    You can retrieve the File information from an <input> element like this (ref):

    <input type="file" id="your-files" multiple>
    <script>
    var control = document.getElementById("your-files");
    control.addEventListener("change", function(event) {
        // When the control has changed, there are new files
        var files = control.files,
        for (var i = 0; i < files.length; i++) {
            console.log("Filename: " + files[i].name);
            console.log("Type: " + files[i].type);
            console.log("Size: " + files[i].size + " bytes");
        }
    }, false);
    </script>
    

    Here is a drag-and-drop version of the above (ref):

    <div id="your-files"></div>
    <script>
    var target = document.getElementById("your-files");
    target.addEventListener("dragover", function(event) {
        event.preventDefault();
    }, false);
    
    target.addEventListener("drop", function(event) {
        // Cancel default actions
        event.preventDefault();
        var files = event.dataTransfer.files,
        for (var i = 0; i < files.length; i++) {
            console.log("Filename: " + files[i].name);
            console.log("Type: " + files[i].type);
            console.log("Size: " + files[i].size + " bytes");
        }
    }, false);
    </script>
    

    Step 2:

    We can now inspect the files and tease out headers and MIME types.

    ✘ Quick method

    You can naïvely ask Blob for the MIME type of whatever file it represents using this pattern:

    var blob = files[i]; // See step 1 above
    console.log(blob.type);
    

    For images, MIME types come back like the following:

    image/jpeg
    image/png
    ...

    Caveat: The MIME type is detected from the file extension and can be fooled or spoofed. One can rename a .jpg to a .png and the MIME type will be be reported as image/png.


    ✓ Proper header-inspecting method

    To get the bonafide MIME type of a client-side file we can go a step further and inspect the first few bytes of the given file to compare against so-called magic numbers. Be warned that it's not entirely straightforward because, for instance, JPEG has a few "magic numbers". This is because the format has evolved since 1991. You might get away with checking only the first two bytes, but I prefer checking at least 4 bytes to reduce false positives.

    Example file signatures of JPEG (first 4 bytes):

    FF D8 FF E0 (SOI + ADD0)
    FF D8 FF E1 (SOI + ADD1)
    FF D8 FF E2 (SOI + ADD2)

    Here is the essential code to retrieve the file header:

    var blob = files[i]; // See step 1 above
    var fileReader = new FileReader();
    fileReader.onloadend = function(e) {
      var arr = (new Uint8Array(e.target.result)).subarray(0, 4);
      var header = "";
      for(var i = 0; i < arr.length; i++) {
         header += arr[i].toString(16);
      }
      console.log(header);
    
      // Check the file signature against known types
    
    };
    fileReader.readAsArrayBuffer(blob);
    

    You can then determine the real MIME type like so (more file signatures here and here):

    switch (header) {
        case "89504e47":
            type = "image/png";
            break;
        case "47494638":
            type = "image/gif";
            break;
        case "ffd8ffe0":
        case "ffd8ffe1":
        case "ffd8ffe2":
        case "ffd8ffe3":
        case "ffd8ffe8":
            type = "image/jpeg";
            break;
        default:
            type = "unknown"; // Or you can use the blob.type as fallback
            break;
    }
    

    Accept or reject file uploads as you like based on the MIME types expected.


    Demo

    Here is a working demo for local files and remote files (I had to bypass CORS just for this demo). Open the snippet, run it, and you should see three remote images of different types displayed. At the top you can select a local image or data file, and the file signature and/or MIME type will be displayed.

    Notice that even if an image is renamed, its true MIME type can be determined. See below.

    Screenshot

    Expected output of demo


    // Return the first few bytes of the file as a hex string
    function getBLOBFileHeader(url, blob, callback) {
      var fileReader = new FileReader();
      fileReader.onloadend = function(e) {
        var arr = (new Uint8Array(e.target.result)).subarray(0, 4);
        var header = "";
        for (var i = 0; i < arr.length; i++) {
          header += arr[i].toString(16);
        }
        callback(url, header);
      };
      fileReader.readAsArrayBuffer(blob);
    }
    
    function getRemoteFileHeader(url, callback) {
      var xhr = new XMLHttpRequest();
      // Bypass CORS for this demo - naughty, Drakes
      xhr.open('GET', '//cors-anywhere.herokuapp.com/' + url);
      xhr.responseType = "blob";
      xhr.onload = function() {
        callback(url, xhr.response);
      };
      xhr.onerror = function() {
        alert('A network error occurred!');
      };
      xhr.send();
    }
    
    function headerCallback(url, headerString) {
      printHeaderInfo(url, headerString);
    }
    
    function remoteCallback(url, blob) {
      printImage(blob);
      getBLOBFileHeader(url, blob, headerCallback);
    }
    
    function printImage(blob) {
      // Add this image to the document body for proof of GET success
      var fr = new FileReader();
      fr.onloadend = function() {
        $("hr").after($("<img>").attr("src", fr.result))
          .after($("<div>").text("Blob MIME type: " + blob.type));
      };
      fr.readAsDataURL(blob);
    }
    
    // Add more from http://en.wikipedia.org/wiki/List_of_file_signatures
    function mimeType(headerString) {
      switch (headerString) {
        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;
    }
    
    function printHeaderInfo(url, headerString) {
      $("hr").after($("<div>").text("Real MIME type: " + mimeType(headerString)))
        .after($("<div>").text("File header: 0x" + headerString))
        .after($("<div>").text(url));
    }
    
    /* Demo driver code */
    
    var imageURLsArray = ["http://media2.giphy.com/media/8KrhxtEsrdhD2/giphy.gif", "http://upload.wikimedia.org/wikipedia/commons/e/e9/Felis_silvestris_silvestris_small_gradual_decrease_of_quality.png", "http://static.giantbomb.com/uploads/scale_small/0/316/520157-apple_logo_dec07.jpg"];
    
    // Check for FileReader support
    if (window.FileReader && window.Blob) {
      // Load all the remote images from the urls array
      for (var i = 0; i < imageURLsArray.length; i++) {
        getRemoteFileHeader(imageURLsArray[i], remoteCallback);
      }
    
      /* Handle local files */
      $("input").on('change', function(event) {
        var file = event.target.files[0];
        if (file.size >= 2 * 1024 * 1024) {
          alert("File size must be at most 2MB");
          return;
        }
        remoteCallback(escape(file.name), file);
      });
    
    } else {
      // File and Blob are not supported
      $("hr").after( $("<div>").text("It seems your browser doesn't support FileReader") );
    } /* Drakes, 2015 */
    img {
      max-height: 200px
    }
    div {
      height: 26px;
      font: Arial;
      font-size: 12pt
    }
    form {
      height: 40px;
    }
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
    <form>
      <input type="file" />
      <div>Choose an image to see its file signature.</div>
    </form>
    <hr/>

    0 讨论(0)
  • 2020-11-22 04:43

    As Drake states this could be done with FileReader. However, what I present here is a functional version. Take in consideration that the big problem with doing this with JavaScript is to reset the input file. Well, this restricts to only JPG (for other formats you will have to change the mime type and the magic number):

    <form id="form-id">
      <input type="file" id="input-id" accept="image/jpeg"/>
    </form>
    
    <script type="text/javascript">
        $(function(){
            $("#input-id").on('change', function(event) {
                var file = event.target.files[0];
                if(file.size>=2*1024*1024) {
                    alert("JPG images of maximum 2MB");
                    $("#form-id").get(0).reset(); //the tricky part is to "empty" the input file here I reset the form.
                    return;
                }
    
                if(!file.type.match('image/jp.*')) {
                    alert("only JPG images");
                    $("#form-id").get(0).reset(); //the tricky part is to "empty" the input file here I reset the form.
                    return;
                }
    
                var fileReader = new FileReader();
                fileReader.onload = function(e) {
                    var int32View = new Uint8Array(e.target.result);
                    //verify the magic number
                    // for JPG is 0xFF 0xD8 0xFF 0xE0 (see https://en.wikipedia.org/wiki/List_of_file_signatures)
                    if(int32View.length>4 && int32View[0]==0xFF && int32View[1]==0xD8 && int32View[2]==0xFF && int32View[3]==0xE0) {
                        alert("ok!");
                    } else {
                        alert("only valid JPG images");
                        $("#form-id").get(0).reset(); //the tricky part is to "empty" the input file here I reset the form.
                        return;
                    }
                };
                fileReader.readAsArrayBuffer(file);
            });
        });
    </script>
    

    Take in consideration that this was tested on latest versions of Firefox and Chrome, and on IExplore 10.

    For a complete list of mime types see Wikipedia.

    For a complete list of magic number see Wikipedia.

    0 讨论(0)
提交回复
热议问题