Page 1 of 2 When working with graphics eventually you need to read or write data to the local file system. In this extract from a chapter in my new book on JavaScript Graphics we look at how it works.
Now available as a paperback or ebook from Amazon.
JavaScript Bitmap Graphics With Canvas
Contents
- JavaScript Graphics
- Getting Started With Canvas
- Drawing Paths
Extract: Basic Paths Extract: SVG Paths Extract: Bezier Curves
- Stroke and Fill
Extract: Stroke Properties Extract: Fill and Holes Extract: Gradient & Pattern Fills
- Transformations
Extract: Transformations Extract: Custom Coordinates Extract Graphics State
- Text
Extract: Text, Typography & SVG Extract: Unicode
- Clipping, Compositing and Effects
Extract: Clipping & Basic Compositing
- Generating Bitmaps
Extract: Introduction To Bitmaps Extract : Animation
- WebWorkers & OffscreenCanvas
Extract: Web Workers Extract: OffscreenCanvas
- Bit Manipulation In JavaScript
Extract: Bit Manipulation
- Typed Arrays
Extract: Typed Arrays
- Files, blobs, URLs & Fetch
Extract: Blobs & Files Extract: Read/Writing Local Files Extract: Fetch API **NEW!
- Image Processing
Extract: ImageData Extract:The Filter API
- 3D WebGL
Extract: WebGL 3D
- 2D WebGL
Extract: WebGL Convolutions
<ASIN:B07XJQDS4Z>
<ASIN:1871962579>
<ASIN:1871962560>
When working with bitmaps and many other similar resources there is a common problem of how do we actually load or save the resource? In simple cases the standard method is to use a URL within an HTML tag. For example, setting the src property of an img tag to a URL causes the browser to request the file, the server to send it and the browser to display it. If you want to do the same in JavaScript, so that you can process the file before it is displayed, then you need to know how to work with files and their precursor, the blob. If you want to create files within JavaScript you also need to know how to create URLs that reference data within the program.
This chapter looks at the problem of working with files, specifically image files from JavaScript. Although the emphasis is on image files, the ideas are general and are applicable to any type of file with slight modifications.
In Chapter but not in this extract
FileReader
At the moment we have the File object, but don’t have access to its data. All we have is its name and some information about it. If you want to get the data you have to use a FileReader to actually read the data into the program. You create a FileReader object using a constructor:
var fileReader=new FileReader();
Once you have a FileReader you can use one of its methods to get the data in the format you desire:
readAsArrayBuffer(file)
readAsDataURL(file)
readAsText(file)
The DataURL format will be explained later and the rest should be obvious. The file parameter specifies the file object to read. This is an asynchronous operation and when it is complete the load event is triggered and the data are available as e.target.result or equivalently as the result property of the FileReader.
Of course, the result is in the specified format and appropriate object. If things go wrong then the error event is raised. You can also call the abort method and this stops the reading of the file and raises onabort when it is completed. The readyState property also indicates the progress of the reading:
-
0 EMPTY Reader has been created. None of the read methods called yet.
-
1 LOADING A read method has been called.
-
2 DONE The operation is complete.
You can also use a FileReader to read a blob. In this case it is mostly a matter of converting the Blob or File to another data type.
Reading a Local File
If the File object is derived from the user selecting a file on the local machine then you really are reading in data that wasn’t part of your program. For example:
files.addEventListener("change", handleFiles, false);
function handleFiles(e) {
var file = this.files[0];
var fileReader = new FileReader();
fileReader.addEventListener("load", fileRead, false);
fileReader.readAsText(file);
}
function fileRead(e) {
console.log(e.target.result);
}
lists the contents of any text file the user cares to select on the console.
While this approach works it uses two event handlers. A more modern way of doing the job is to create functions which wrap each of the tasks of getting the File object and reading the File object.
The getFile function returns a File object when the user selects a file. To be more accurate it returns a Promise that resolves when the user selects a file:
function getFile() {
return new Promise(function (resolve, reject) {
files.addEventListener("change", function (e) {
var file = e.target.files[0];
resolve(file);
});
});
}
The readFile function returns a Promise that resolves to the contents of the File object passed to it:
function readFile(file) {
return new Promise(function (resolve, reject) {
var fileReader = new FileReader();
fileReader.addEventListener("load", function (e) {
resolve(e.target.result);
}, false);
fileReader.readAsText(file);
}); }
Notice how in both cases the event handlers call the promise’s resolve method with the result of the task.
Now we have these two “modernized” functions we can either write code that makes use of their promises or, even more modern and much better, use async and await:
async function displayFile() {
var file = await getFile();
var result = await readFile(file);
console.log(result);
}
Now to get the contents of a user selected file we simply call displayFile. Notice that you have to call getFile and readFile from within a function as await doesn’t work in the main program.
Wrapping old legacy calls and event handlers in a Promise function is the best way to organize things. You can also add calls to the promise’s reject function if there is an error and handle the errors in the displayFile function using try catch.
As a final example, let’s get a graphics file from the local machine and display it, ready to be manipulated, in a canvas object. In this case we need a modified version of the reading function:
function readGraphic(file) {
return new Promise(function (resolve, reject) {
var fileReader = new FileReader();
fileReader.addEventListener("load",
function (e) {
resolve(e.target.result);
}, false);
fileReader.readAsDataURL(file);
});
}
We could read the file as an array or string of values, but then we would have to process the file according to its type and extract the raw image data. This can be done – see the example of reading a PCX file in the next chapter – but it is usually unnecessary. Instead we read the file as a data URL because we can use this to get an Image object to read and decode the file for us. For the moment you can think of a data URL as being a URL that encodes the associated data so that it can be read like a file.
Now that we have the data URL we can use it to load an Image object and then draw this to the canvas:
async function displayGraphic(ctx) {
var file = await getFile();
var url = await readGraphic(file);
var img = new Image();
img.src = url;
await imgLoaded(img);
ctx.drawImage(img, 0, 0);
}
The imgLoaded function was given in an earlier chapter:
function imgLoaded(img) {
return new Promise(
function (resolve, reject) {
img.addEventListener("load", function () {
resolve(img);
});
})
}
In fact, there is a more direct way of doing the same job without using FileReader. A File object can be directly converted to an object URL and this can be assigned to an Image object which will do the download and the conversion to a bitmap in one step. To use this method we need to modify readGraphic to return an Image object:
async function readGraphic(file) {
return new Promise(
function (resolve, reject) {
var url = URL.createObjectURL(file);
var img = new Image();
img.addEventListener("load", function (e) {
resolve(img);
URL.revokeObjectURL(url);
}, false);
img.src = url;
});
}
Using this displayGraphic is just:
async function displayGraphic(ctx) {
var file = await getFile();
var img = await readGraphic(file);
ctx.drawImage(img, 0, 0);
}
This is not only simple, it is also faster as constructing a data URL can be slow.
The general point is that an Image object already knows how to load and decode many different types of graphics files and, unless you need access to the raw file data, it is better to use it to load graphics in preference to FileReader.
|