Saving files locally using Blob and msSaveBlob

This topic starts where Saving files locally using Web Storage left off, and demonstrates how to locally save files (of arbitrary size) using the Blob constructor along with the window.navigator.msSaveBlob and window.navigator.msSaveOrOpenBlob methods.

This topic includes the following sections:

  • Creating blobs using the BlobBuilder API
  • Related topics

Note  The following examples require Internet Explorer 10 or later.

 

Creating blobs using the BlobBuilder API

The Blob constructor allows you to easily create and manipulate a blob (typically equivalent to a file) directly on the client. Internet Explorer 10's msSaveBlob and msSaveOrOpenBlob methods allow a user to save the file on the client as if the file had been downloaded from the Internet (which is why such files are saved to the Downloads folder).

If you're wondering, the difference between the msSaveBlob and msSaveOrOpenBlob methods is that the former only provides a Save button to the user whereas the latter provides both a Save and an Open button.

To help understand how the Blob constructor and msSaveBlob/msSaveOrOpenBlob can be used to save an altered file on the client, consider the following example:

Example 1

<!DOCTYPE html>
<html>

<head>
  <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
  <title>Example 1</title>
</head>

<body>
  <h1>Example 1</h1>
  <script>
    var blobObject = new Blob(["I scream. You scream. We all scream for ice cream."]); 
    
    window.navigator.msSaveBlob(blobObject, 'msSaveBlob_testFile.txt'); // The user only has the option of clicking the Save button.
    alert('File save request made using msSaveBlob() - note the single "Save" button below.');
    
    var fileData = ["Before you insult a person, walk a mile in their shoes. That way, when you insult them, you'll be a mile away - and have their shoes."];
    blobObject = new Blob(fileData);
    window.navigator.msSaveOrOpenBlob(blobObject, 'msSaveBlobOrOpenBlob_testFile.txt'); // Now the user will have the option of clicking the Save button and the Open button.
    alert('File save request made using msSaveOrOpenBlob() - note the two "Open" and "Save" buttons below.');
  </script>
</body>

</html>

Using the Blob()constructor, we first create a blob object whose parameter is an array containing the desired file content:

var blobObject = new Blob(["I scream. You scream. We all scream for ice cream."]);

We next copy the content from blobObject and save it to a text file (which is saved within the Downloads folder):

window.navigator.msSaveBlob(blobObject, 'msSaveBlob_testFile.txt');

Note  msSaveBlob_testFile.txt can only be saved to the Downloads folder if the user provides permission to do so (by clicking the associated prompt in the Information bar appropriately).

 

This process is then repeated using the msSaveOrOpenBlob method, which provides the user with both a Save and an Open option.

The next example adds Blob feature detection:

Example 2

<!DOCTYPE html>
<html>

<head>
  <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
  <title>Example 2</title>
</head>

<body>
  <h1>Example 2</h1>
  <script>
    function requiredFeaturesSupported() {
      return ( BlobConstructor() && msSaveOrOpenBlobSupported() );
    }
    
    function BlobConstructor() {
      if (!window.Blob) {
        document.getElementsByTagName('body')[0].innerHTML = "<h1>The Blob constructor is not supported - upgrade your browser and try again.</h1>";
        return false;
      } // if
      
      return true;
    } // BlobConstructor
    
    function msSaveOrOpenBlobSupported() {
      if (!window.navigator.msSaveOrOpenBlob) { // If msSaveOrOpenBlob() is supported, then so is msSaveBlob().
        document.getElementsByTagName('body')[0].innerHTML = "<h1>The msSaveOrOpenBlob API is not supported - try upgrading your version of IE to the latest version.</h1>";            
        return false;
      } // if
      
      return true;
    } // msSaveOrOpenBlobSupported
        
    if (requiredFeaturesSupported()) {
      var blobObject = new Blob(["I scream. You scream. We all scream for ice cream."]);
      
      window.navigator.msSaveBlob(blobObject, 'msSaveBlob_testFile.txt');
      alert('File save request made using msSaveBlob() - note the single "Save" button below.');
      
      var fileData = ["Before you insult a person, walk a mile in their shoes. That way, when you insult them, you'll be a mile away - and have their shoes."];
      blobObject = new Blob(fileData);
      window.navigator.msSaveOrOpenBlob(blobObject, 'msSaveBlobOrOpenBlob_testFile.txt');
      alert('File save request made using msSaveOrOpenBlob() - note the two "Open" and "Save" buttons below.');
    }
  </script>
</body>

</html>

After a file has been saved to the client, the next step is to retrieve data from the saved file. The following example, based on Reading local files and Saving files locally using Web Storage, allows you to create a simple canvas-based drawing, save it to a local file, and display such a saved drawing. This example is typically used as follows:

  1. Using your mouse or finger (touch devices only), create a drawing within the box.
  2. Click the Save button, then click the resulting Save button within the Information bar.
  3. Dismiss the second Information bar by clicking x.
  4. Click the Erase button.
  5. Click the Load button, then click the resulting Browse button and select a previously saved drawing file. The saved drawing is displayed.

Be aware that after a drawing has been saved, skipping step 4 allows a user to composite multiple drawings.

Example 3

<!DOCTYPE html>
<html>

<head>
  <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
  <title>Example 3</title>
  <style>
    html { 
     -ms-touch-action: none; /* Capture all touch events for our own purposes. */
     text-align: center;
    }
  
    #hideWrapper { 
      display: none; /* Do not show the file picker dialog until we're ready to do so. */
    }    
  </style>
</head>

<body>
  <h1>Example 3</h1>
  <canvas id="drawingSurface" width="500" height="500" style="border:1px solid black;">
  </canvas> <!-- The canvas element can only be manipulated via JavaScript -->
  <div>
    <button id="erase">Erase</button>
    <button id="save">Save</button>
    <button id="load">Load</button>
  </div>  
  <div id="hideWrapper">
    <p>Select one of your saved canvas drawings to display:</p>
    <input type="file" id="fileSelector" /> <!-- By design, if you select the exact same files two or more times, the 'change' event will not fire. -->
  </div>    
  <script>
    function requiredFeaturesSupported() {
      return ( 
               BlobConstructorSupported() && 
               msSaveOrOpenBlobSupported() && 
               canvasSupported() && 
               fileApiSupported()
             );
    } // requiredFeaturesSupported
    
    function BlobConstructorSupported() {
      if (!window.Blob) {
        document.getElementsByTagName('body')[0].innerHTML = "<h1>The Blob constructor is not supported - upgrade your browser and try again.</h1>";
        return false;
      } // if
      
      return true;
    } // BlobConstructorSupported
    
    function msSaveOrOpenBlobSupported() {
      if (!window.navigator.msSaveOrOpenBlob) { // If msSaveOrOpenBlob() is supported, then so is msSaveBlob().
        document.getElementsByTagName('body')[0].innerHTML = "<h1>The msSaveOrOpenBlob API is not supported - upgrade Internet Explorer and try again.</h1>";            
        return false;
      }
      
      return true;
    } // msSaveOrOpenBlobSupported
        
    function canvasSupported() {
      if (!document.createElement('canvas').getContext) {
        document.getElementsByTagName('body')[0].innerHTML = "<h1>Canvas is not supported - upgrade your browser and try again.</h1>";                  
        return false;
      }
      
      return true;
    } // canvasSupported  
    
    function fileApiSupported() {
      if (document.getElementById('fileSelector').files && window.FileReader) {
        return true;        
      }
      else {
        document.getElementsByTagName('body')[0].innerHTML = "<h1>The File API is not sufficiently supported - upgrade your browser and try again.</h1>";            
        return false;      
      }
    } // fileSelectorSupported      
        
    if ( requiredFeaturesSupported() ) {    
      var canvas = document.getElementById('drawingSurface'); // A static variable, due to the fact that one or more local functions access it.
      var context = canvas.getContext('2d'); // A static variable, due to the fact that one or more local functions access it.

      context.fillStyle = "purple"; // Because purple is cool.

      if (window.navigator.msPointerEnabled) {
        canvas.addEventListener('MSPointerMove', paintCanvas, false);
      }
      else {
        canvas.addEventListener('mousemove', paintCanvas, false);
      }

      document.getElementById('erase').addEventListener('click', eraseCanvas, false);
      document.getElementById('save').addEventListener('click', saveCanvas, false);
      document.getElementById('load').addEventListener('click', loadCanvas, false);
      
      document.getElementById('fileSelector').addEventListener('change', handleFileSelection, false); // Add an onchange event listener for the <input id="fileSelector"> element.      
    } // if ( requiredFeaturesSupported() )
    
    function paintCanvas(event) { // The "event" object contains the position of the pointer/mouse.
      context.fillRect(event.offsetX, event.offsetY, 4, 4); // Draw a 4x4 rectangle at the given coordinates (relative to the canvas box). As of this writing, not all browsers support offsetX and offsetY.
    }

    function saveCanvas() {
      var drawingFileName = "canvas" + Math.round( (new Date()).getTime() / 1000 ) + ".txt"; // Produces a unique file name every second.
      var blobObject = new Blob( [canvas.toDataURL()] ); // Create a blob object containing the user's drawing.
      
      window.navigator.msSaveBlob(blobObject, drawingFileName); // Copy the blob object content and save it to a file.
      document.getElementById('hideWrapper').style.display = 'none'; // Remove the file picker dialog from the screen if the Save button gets clicked.      
    } // saveCanvas

    function eraseCanvas() {
      context.clearRect(0, 0, context.canvas.width, context.canvas.height);
      document.getElementById('hideWrapper').style.display = 'none'; // Remove the file picker dialog from the screen if the Erase button gets clicked.
    } // eraseCanvas

    function loadCanvas() {
      document.getElementById('hideWrapper').style.display = 'block'; // Unhide the file picker dialog so the user can select a saved canvas drawing to load into the canvas element.   
    } // loadCanvas      
    
    function handleFileSelection(evt) {    
      var files = evt.target.files; // The file selected by the user (as a FileList object).

      if (!files) {
        alert("The selected file is invalid - do not select a folder. Please reselect and try again.");
        return;
      }

      // "files" is a FileList of file objects. Try to display the contents of the selected file:
      var file = files[0]; // The way the <input> element is set up, the user cannot select multiple files.
      
      if (!file) {
        alert("Unable to access " + file.name.toUpperCase() + "Please reselect and try again."); 
        return;
      }
      if (file.size == 0) {
        alert("Unable to access " + file.name.toUpperCase() + " because it is empty. Please reselect and try again.");
        return;
      }
      if (!file.type.match('text/.*')) {
        alert("Unable to access " + file.name.toUpperCase() + " because it is not a known text file type. Please reselect and try again.");
        return;
      }
      
      // Assert: we have a valid file.
      
      startFileRead(file); // Asychronously fire off a file read request.      
      document.getElementById('hideWrapper').style.display = 'none'; // Remove the file picker dialog from the screen since we have a valid file.      
    } // handleFileSelection
    
    function startFileRead(fileObject) {
      var reader = new FileReader(); //

      // Set up asynchronous handlers for file-read-success, file-read-abort, and file-read-errors:
      reader.onloadend = displayDrawing; // "onloadend" fires when the file contents have been successfully loaded into memory.
      reader.abort = handleFileReadAbort; // "abort" files on abort.
      reader.onerror = handleFileReadError; // "onerror" fires if something goes awry.

      if (fileObject) { // Safety first.
        reader.readAsText(fileObject); // Asynchronously start a file read thread. Other supported read methods include readAsArrayBuffer() and readAsDataURL().
      }
      else {
        alert("fileObject is null in startFileRead().");
      }
    } // startFileRead
    
    function displayDrawing(evt) {
      var img = new Image(); // The canvas drawImage() method expects an image object.
  
      img.src = evt.target.result; // Obtain the file contents, which was read into memory (whose format is a text data URL string).
      // eraseCanvas(); To allow composite drawings, remove this comment.
      img.onload = function() { // Only render the saved drawing when the image object has fully loaded the drawing into memory.
        context.drawImage(img, 0, 0); // Draw the image starting at canvas coordinate (0, 0) - the upper left-hand corner of the canvas.
      } // img.onload */
    } // displayFileText
      
    function handleFileReadAbort(evt) {
      alert("File read aborted.");
    } // handleFileReadAbort

    function handleFileReadError(evt) {
      switch (evt.target.error.name) {
        case "NotFoundError":
          alert("The file could not be found at the time the read was processed.");
          break;
        case "SecurityError":
          alert("A file security error occured.");
          break;
        case "NotReadableError":
          alert("The file cannot be read. This can occur if the file is open in another application.");
          break;
        case "EncodingError":
          alert("The length of the data URL for the file is too long.");
          break;
        default:
          alert("File error code " + evt.target.error.name);
      } // switch
    } // handleFileReadError
  </script>
</body>

</html>

If you haven't already noticed, one of the features of example 3 is that you can't consecutively reload the same drawing file two times. That is, the following procedure fails:

  1. Click Erase if a drawing is present.
  2. Using your mouse or finger (touch devices only), create a drawing within the box.
  3. Click the Save button, then click the resulting Save button within the Information bar.
  4. Dismiss the second Information bar by clicking x.
  5. Click the Erase button.
  6. Click the Load button, then click the resulting Browse button and select the drawing file that was created in step 3. The saved drawing is displayed.
  7. Click the Erase button.
  8. Click the Load button, then click the resulting Browse button and select the same drawing file that was selected in step 6. The drawing is not displayed.

This behavior is by design, but you can easily work around it. The first step is to place the <input type="file" id="fileSelector" /> element within a <form> element, as follows:

<div id="hideWrapper">
  <p>Select one of your saved canvas drawings to display:</p>
  <form>
    <input type="file" id="fileSelector" />
  </form>
</div>    
<script>

The next step is to clear any prior user input from the form's child elements by calling the form's reset() method whenever the Load button is clicked:

function loadCanvas() {
  document.querySelector('#hideWrapper > form').reset(); // Allow the input element to pick the same file consecutively more than once.    
  document.getElementById('hideWrapper').style.display = 'block'; // Unhide the file picker dialog so the user can select a saved canvas drawing to load into the canvas element.   
} // loadCanvas

This updated example is available here: Example 4 (right-click the webpage and choose View source to view its source code).

Similarly, example 5 extends example 4 by improving the simplistic drawing "app", and by replacing canvas.toDataURL() with canvas.msToBlob(). By using canvas.toDataURL(), you preclude the possibility of easily using another application to view a saved drawing. Saving the drawing as a PNG file has the benefit of allowing a number of standard applications, including the browser, to display the drawing. By switching to canvas.msToBlob(), we can save the file directly to PNG format as follows:

function saveCanvas() {
  var drawingFileName = "canvas" + Math.round( (new Date()).getTime() / 1000 ) + ".png"; // Produces a unique file name every second.
  
  window.navigator.msSaveBlob(globals.canvas.msToBlob(), drawingFileName); // Save the user's drawing to a file.
  document.getElementById('filePickerWrapper').style.display = 'none'; // Remove the file picker dialog from the screen since we just saved the user's file.
} // saveCanvas

Additionally, saving the file in PNG format allows us to replace startFileRead(file) (and its three associate callback functions) with the following four lines of code:

img.src = window.URL.createObjectURL(file);
img.onload = function() { 
  globals.context.drawImage(img, 0, 0); 
  window.URL.revokeObjectURL(this.src); 
}

The full example is available here: Example 5 (right-click the webpage and choose View source to view its source code).

To conclude, using Windows Internet Explorer's msSaveBlob and msSaveOrOpenBlob methods along with the Blob constructor allows you to save modified files on the client - something that was relatively difficult before Internet Explorer 10.

Internet Explorer 10 Samples and Tutorials

File API

How to manage local files

Reading local files

Saving files locally using Web Storage