First, a note from Filament Group: We recently released Grumpicon, a web app for easy use of our command-line SVG workflow, Grunticon. You can read more about Grumpicon in our release post here: Introducing Grumpicon. We'd been wanting to build this application for a while and had yet to find the time to do so. Since this was a tool designed strictly for web developers and we wanted it to be portable enough to easily run anywhere – locally or on a remote server – we decided that the first pass at the app could be built in JavaScript alone, without any reliance on server-side technology. We posted the following requirements for the proposed app in an issue in the Grunticon repo, and posted a link to it for help on Twitter:
It'd be cool if we could drop a folder of svgs on the browser and get a zip file of typical Grunticon output in return. Maybe it could be built without a server using browser tech alone, I'm not sure...
- drag and drop the folder of SVGs onto the browser
- create the first css file based on the escaped contents of the svg
- use canvas's data uri functions to create the png fallback CSS and images from the svg source files
- zip the generated output files using something like https://github.com/Stuk/jszip
- offer a data uri of the zip for download Anyone want to help build this thing?
Much to our good fortune, Eric Ponto stepped up to the challenge. We asked Eric to write a guest post on the inner workings of the Grumpicon app for those interested in the technical aspects of its codebase.
Take it away, Eric!
First, I have to come clean. When Scott tweeted about wanting a web app version of Grunticon a couple of weeks ago, I had never used Grunticon. Actually, still to this day I've never used Grunticon. Shocking...I know.
Of course, I had heard of it. I am a big proponent of using Grunt and use it everywhere to help automate my life. And I follow enough people on Twitter/read enough blogs/listen to enough podcast to have gotten ample exposure to Grunticon and it sounded really cool. So when I saw Scott's tweet I took to action.
Before reading on, if you're not familiar with Grunticon or Grumpicon, check those out first.
Here are kind of the steps on how Grumpicon works:
- Handle drag and dropped SVG files
- Read the SVGs
- Covert SVGs to data URIs
- Covert SVGs to PNG data URIs
- Make CSS files and the preview.html file
- Create a downloaded zip with all the files and PNG images
Sounds easy enough, right?
##Handle drag and dropped SVG files
The drag and drop API has a few nifty events for handling dropping files onto a web app. From Grumpicon we really just use two: drop
and dragover
. (Okay, so technically dragleave
is also used just for removing the page highlighting.) With the dragover event we can add a highlight to the page and tell it to "copy" the file when dropped (instead of opening it). The drop event has a dataTransfer
property that contains files
which is a FileList array on all the files dropped. From there we can do what we want with the files.
Some pseudo code from Grumpicon (check out the Grumpicon GitHub repo for the actual code):
Views.UploadView = Backbone.View.extend({
events: {
"drop": "fileDrop",
"dragover": "fileDrag"
},
fileDrag: function(e) {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = "copy";
// add a class to the body to highlight
},
fileDrop: function(e) {
e.preventDefault();
e.stopPropagation();
// note, jQuery doesn't normalize the "drop" event
// so we have to access originalEvent to get to the dataTransfer property
this.addFiles(e.originalEvent.dataTransfer.files);
},
addFiles: function(fileList) {
// do something with the files
}
});
##Read the SVGs
To do anything with the dropped SVGs we need to access the content of the files. Grunticon generates data URIs with escaped SVG text as background images, we just need to read the text of the files. This is where the File API comes in to play and specifically the FileReader
object.
Reading a file is a lot like create a new image. First you create a new FileReader()
, then you set an onload
event to do something with the content once it has been reader, and finally you tell the reader to read a file as text.
More pseudo code:
read: function() {
var model = this,
file = model.get("file"),
reader = new FileReader();
reader.onload = function(e) {
var svgText = e.target.resutls;
//do stuff with the svgText
};
reader.readAsText(file);
}
##Covert SVGs to data URIs
This step is actually super simple. Just escape the svgText
read from the file, tack on "data:image/svg+xml;charset=US-ASCII,"
to the beginning and blammo.
##Covert SVGs to PNG data URIs
Another amazing HTML5 API saves the day. The canvas
has a nice little method called toDataURL()
which takes the contents of a canvas and generates a PNG data URI...exactly what we want. So the only trick here is drawing the SVG onto a canvas.
Luckily, it was canvg to the rescue. This small library makes drawing a SVG on a canvas super easy. Then toDataURL()
can be called on the canvas.
canvg(canvas, svgText);
var pngDataUri = canvas.toDataURL();
##Make CSS files and the preview.html file The 3 CSS files and the HTML file are just made on the page using Underscore templates using the data from the dropped SVGs and the data URIs generated.
##Create a downloaded zip with all the files and PNG images When I first started thinking about how to make Grunticon work in the browser, this was the step I was most unsure of. Creating zips? Node sure, but on the client...
JSZip - another library to the rescue. It's a truly remarkable library. You can add all type of files to your zip, including images from a data URI (perfect for our needs), and even create folders.
var view = this,
zip = new JSZip(),
// create folder
img = zip.folder("png");
view.collection.each(function(model) {
// add images to the folder
img.file(model.get("name") + ".png",
model.get("pngDataUri").replace("data:image/png;base64,","") + "\n",
{base64: true});
});
// add the other files
zip.file("icons.data.svg.css", $("#svg-css-results").text());
zip.file("icons.data.png.css", $("#png-css-results").text());
zip.file("icons.fallback.css", $("#fallback-css-results").text());
zip.file("grunticon.loader.txt", ...);
zip.file("preview.html", ...);
// create the zip
var content = zip.generate();
// make the browser download it
location.href = "data:application/zip;base64," + content;
So that's basically it. To see the actual source, check out the Grumpicon repo on GitHub.
P.S. I had an amazing time working on this app with the team from Filament Group. It's the most fun I've had developing anything in a quite a while!
###All of JS Libraries used (Thank you open source!)
Forked here in MD format for editing