tl;dr: Github Sample
This is a question that comes up quite often amongst developers, and for some reason is a question without many complete solutions out there. This article aims to be a complete solution.
For starters, let's take a look at the following code.
canvas.toBlob(
function (blob) {
fileWriter.write(blob);
},
'image/png'
);
Ultimately, this code is the crux of the problem. In the code above:
However, toBlob isn't quite as magical as it might first seem. In fact, it's practically standard:
http://www.w3.org/TR/2011/WD-html5-20110525/the-ca
The main problem here is that not many browsers have implemented the <canvas> toBlob functionality, so we have to go looking elsewhere.
Luckily, Eli Grey has provided the HTML5 standard as a cross-browser implementation on Github® under the MIT/X11 license:
https://github.com/eligrey/canvas-toBlob.js
So, how do we demystify toBlob? Easy:
<script type="text/javascript" src="https://raw.github.com/eligrey/canvas-toBlob.js/master/canvas-toBlob.min.js"></script>
All that remains now is coming up with a valid fileWriter instance to perform the actual save of our newly converted blob object. For this, we'll start at the beginning.
Before we can write to a filesystem, we need to gain access to the filesystem. In fact, there are some very handy HTML5 FileSystem APIs to help us do this. So the first question that arises is, "Where do we want to save our file?"
We could provide the user with a text box to enter the destination, or use some hard-coded values. In fact, the BlackBerry® WebWorks™ APIs provide some handy shortcuts for use on that front:
https://developer.blackberry.com/html5/apis/blackb
But what about that BlackBerry® 10 experience? Well, through use of the Invocation Framework, we can actually present the user with a file browser to choose the location of their saved file. In our case, we'll default to the shared folder outside of our application sandbox.
blackberry.invoke.card.invokeFilePicker(
{
mode: blackberry.invoke.card.FILEPICKER_MODE_SAVER,
type: [
blackberry.invoke.card.FILEPICKER_TYPE_PICTURE
],
directory: [blackberry.io.sharedFolder]
},
function (path) {
/* User choses a path, and we now have a variable referencing it! */
},
function (reason) {
/* User cancelled. */
console.log('User Cancelled: ' + reason);
},
function (error) {
/* Invoked. */
if (error) {
console.log('Invoke Error: ' + error);
}
}
);
In total, we're passing four (4) arguments:
So now we know where we'll be saving the image file, but we don't have a how. For this, we need to obtain an instance of the HTML5 FileSystem. The following code will be called within our function (path) implementation above.
/* User chose a path, and we now have a variable referencing it! */
blackberry.io.sandbox = false; window.webkitRequestFileSystem( window.PERSISTENT, 5.0 * 1024 * 1024, function (fileSystem) {
/* We were granted a FileSystem object! */ }, function (fileError) { /* Error. */ console.log('FileSystem Error: ' + fileError); } );
The call to blackberry.io.sandbox is BlackBerry 10 specific and is what enables us to access the filesystem outside of our application's self-contained sandbox.
Next, we call the WebKit implementation of the HTML5 requestFileSystem API and pass a few arguments. Most importantly, the third argument function (fileSystem) is what will be triggered if the operating system successfully grants us access. If you are not granted access, you may be missing some permissions; refer to the Github sample at the end of this document for a complete, integrated solution.
Now that we have the fileSystem, we can leverage it to obtain a fileEntry. The following code will be called within our function (fileSystem) implementation above.
/* We were granted a FileSystem object. */
fileSystem.root.getFile(
path,
{
create: true
},
function (fileEntry) {
/* We were granted a FileEntry object! */
},
function (fileError) {
console.log('DirectoryEntry (fileSystem.root) Error: ' + fileError);
}
);
First, note the reference to path. This is the path variable that was initially provided from the File Picker APIs. We also pass a flag to indicate that if the file does not exist, we want it to be created. Finally, we have two function arguments; the first if we succeed and the second if there is an error.
Now that we have a fileEntry that represents our desired path we can finally create a FileWriter instance; this is what will actually allow us to write our blob to the filesystem. The following code will be called within our function (fileEntry) implementation above.
/* We were granted a FileEntry object. */
fileEntry.createWriter(
function (fileWriter) {
/* We were granted a FileWriter object! */
},
function (fileError) {
console.log('FileEntry Error: ' + fileError);
}
);
This one is the most straight-forward; we have a success callback and an error callback. On success, we will have the fileWriter object that we need. The last thing to do to save our canvas. The following code will be called within our function (fileWriter) implementation above.
/* We were granted a FileWriter object. */
fileWriter.onerror = function (fileError) {
console.log('FileWriter Error: ' + fileError);
};
fileWriter.onwriteend = function () {
blackberry.ui.toast.show('Canvas saved!');
};
canvas.toBlob(
function (blob) {
fileWriter.write(blob);
},
'image/png'
);
First, we've implemented the onerror and onwriteend functions of the FileWriter object to ensure that we can properly monitor what is going on. Following that, we call the same code snippet that we started with; again taking the <canvas> instance and converting it to a blob, then leveraging our fileWriter (which now exists and references our desired path) to actually save those contents to the filesystem.
In the end, the solution is slightly longer than originally advertised, but should hopefully help guide developers in overcoming this common hurdle.
If you do have any feedback or questions regarding this sample, do not hesitate to post your comments or reach out directy via Twitter (@WaterlooErik).