├── .gitignore ├── data.json ├── package.json ├── newshots.js ├── README.md └── pdf.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | o 3 | -------------------------------------------------------------------------------- /data.json: -------------------------------------------------------------------------------- 1 | { 2 | "webPath": "http://localhost:3000/application/mvp/", 3 | "output": "atwsa-screens.pdf", 4 | "keepPNGs": true, 5 | "browserWidth": 960, 6 | "resize" : { 7 | "full_width": 1280, 8 | "thumb_width": 320 9 | }, 10 | "pages": [ 11 | "index", 12 | "page2", 13 | "page3", 14 | "page4" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shots", 3 | "version": "1.0.0", 4 | "description": "A way to quickly create a PDF of screens of a gov.uk prototype.", 5 | "main": "pdf.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "newshots": "node newshots.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/morganesque/shots.git" 13 | }, 14 | "author": "Tom Morgan", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/morganesque/shots/issues" 18 | }, 19 | "homepage": "https://github.com/morganesque/shots#readme", 20 | "dependencies": { 21 | "colors": "^1.1.2", 22 | "pdfkit": "^0.7.1", 23 | "prompt": "^1.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /newshots.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var prompt = require('prompt') 3 | var exec = require('child_process').exec 4 | 5 | // generic output folder for shots 6 | var outputdir = __dirname + "/o" 7 | // if it doesn't exist create it 8 | if (!fs.existsSync(outputdir)) fs.mkdirSync(outputdir); 9 | 10 | prompt.colors = false 11 | prompt.start() 12 | prompt.get([{ 13 | name: 'newfolder', 14 | description: 'Name', 15 | required: true, 16 | type: 'string' 17 | }],function(err, result) 18 | { 19 | // specific output dir 20 | var shotsdir = outputdir + "/" + result.newfolder 21 | // if it doesn't exist create it 22 | if (!fs.existsSync(shotsdir)) fs.mkdirSync(shotsdir); 23 | // copy example datafile into it. 24 | fs.writeFileSync(shotsdir+'/data.json', fs.readFileSync("data.json")); 25 | // open data file. 26 | exec("atom-beta "+shotsdir+'/data.json',function(err,stdout,stderr) 27 | { 28 | if (err) console.log(err) 29 | }); 30 | }) 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SHOTS 2 | A way to quickly create a PDF of screens of a gov.uk prototype. 3 | 4 | ## Getting started 5 | 6 | ### Webkit2PNG 7 | Although the script is written in Node it uses a Python script called [webkit2png](http://www.paulhammond.org/webkit2png/) to create the screenshots. Bit weird I know, it means you’ll need to install that first. I think [Homebrew](http://brew.sh/) is the easiest way. 8 | 9 | ```bash 10 | brew install webkit2png 11 | ``` 12 | 13 | > **WARNING!** If you’re on El Crapitan there might be another hurdle to jump to do with added security. You might need to [edit the webkit2png](https://github.com/bendalton/webkit2png/commit/9a96ac8977c386a84edb674ca1518e90452cee88) script to get around this. 14 | 15 | ## mogrify 16 | [Mogrify](http://www.imagemagick.org/script/mogrify.php) is a image processing command which comes with ImageMagick. I found I needed a little more control over the sizing of the resulting PNG files than `webkit2png` was giving me. Sadly though it's another dependancy you'll have to install yourself. 17 | 18 | ```bash 19 | brew install imagemagick 20 | ``` 21 | 22 | If you have any problems with this or simply don't want to resize the images then set the `resize` option to `false` or just delete it completely from the `data.json` file. 23 | 24 | ### Setting up (first time only) 25 | 26 | Once you've got webkit2png installed: 27 | 28 | * [download the zip](https://github.com/morganesque/shots/archive/master.zip) and unpack it. 29 | 30 | * Open up Terminal and `cd` into the resulting folder. 31 | 32 | * Install the node modules using `npm install` 33 | 34 | ### Setting up (each time) 35 | 36 | In order to create a set of screens and/or a PDF there's a couple of things you need to do: 37 | 38 | * Create a folder for them to go into (ie. `mkdir `) 39 | 40 | * Copy `data.json` into this folder. (ie. `cp data.json `) 41 | 42 | * Edit your new `data.json` 43 | 44 | ### data.json 45 | ```json 46 | { 47 | "webPath": "http://domain/path/path/", 48 | "output": "screens.pdf", 49 | "keepPNGs": true, 50 | "browserWidth": 960, 51 | "thumbWidth": 100, 52 | "pages": [ 53 | "page1", 54 | "page2", 55 | "page3", 56 | "page4", 57 | ], 58 | } 59 | ``` 60 | 61 | `webPath` – This is the root of the prototype you’re screen-shotting. That’s just the URL without the final page name at the end. 62 | 63 | `output` - The name of the resulting PDF which will appear in your new folder. 64 | 65 | `keepPNGS` - Can be `true` or `false` and decides whether the PNG screenshots are retained or deleted after the PDF has been created. 66 | 67 | `browserWidth` - The width that the browser will be set to when the screenshot is taken. On my machine the images come out twice this size (maybe because it's a retina mac) but that's better for printing so this is a bonus. 68 | 69 | `thumbWidth` - Set this to the width (in pixels) you want thumbnails to be. Leave it out or set it to `false` for no thumbnails. If `keepPNGS` is false no thumbnails will be made. 70 | 71 | `pages` - This is an array of page names in your prototype. They'll be appended to the webPath to generate the URL of the page to be grabbed. The order is how they’ll appear in the PDF. 72 | 73 | ### Run it 74 | 75 | Enter `node pdf.js ` and sit back and watch the magic happen! 76 | 77 |   78 |   79 |   80 |   81 | 82 | ## Explanation 83 | 84 | OK if you're interested here's a bit of reasoning behind doing this and the specific way it's put together. I read this [blog post about screen shots of gov.uk pages](https://designnotes.blog.gov.uk/2015/10/15/how-and-why-to-print-all-the-things/) and found out about both [Paparazzi!](https://derailer.org/paparazzi/) and [webkit2png](http://www.paulhammond.org/webkit2png/) and then possibly automating the printing out of screens from the prototype I was working on. 85 | 86 | I wanted to achieve the following: 87 | 88 | 1. **Avoid one-by-one printing using an app or a chrome extension** - This is too annoying and time consuming. 89 | 90 | 2. **Be able to hit print on the resulting file or files** - I found I could drag a bunch of images into Preview (on Mac) and either print them or export them to a PDF but longer pages were either shrunk to fit the paper, causing the size of the pages to vary greatly, or they stretched onto the following page meaning another (usually needless) sheet was printed. 91 | 92 | 3. **Have the resulting PDF be in the right order** - This was a kind of side issue but it cropped up a couple of times when members of my team needed a quick PDF of the prototype for something and I ended up manually reordering a PDF in Preview page-by-page. 93 | 94 | So having the a data file which could both define the pages to be printed but also the order to print them seemed to work best. Creating a new folder for each different screen grabbing felt like a good way to go and then maintaining the option to retain or simply delete the PNGs that were created along the way seemed like a useful thing too. 95 | -------------------------------------------------------------------------------- /pdf.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec, 2 | fs = require('fs'), 3 | pdfdoc = require('pdfkit'), 4 | colors = require('colors'), 5 | dir = process.argv.slice(2)[0], 6 | done = 0, data, json, pages; 7 | 8 | // check whether they've passed a folder name in the command. 9 | if (typeof dir == 'undefined') { 10 | console.log('Please pass in a folder name.'.red); 11 | process.exit(); 12 | } 13 | 14 | /* 15 | 'Reading file ' 16 | 'generating screenshots ' 17 | 'adding to PDF ' 18 | 'resizing images ' 19 | 'resizing thumbs ' 20 | */ 21 | 22 | // remove any trailing slash from the folder name. 23 | if (dir.substr(-1) === '/') dir = dir.substr(0, dir.length - 1); 24 | 25 | try { 26 | // right now check whether the folder exists. 27 | var s = fs.statSync(dir); 28 | // check the folder is in fact an actual folder. 29 | if (s.isDirectory()) 30 | { 31 | // grab the data file from the folder. 32 | var datafile = __dirname + '/' + dir + '/data.json'; 33 | console.log('Reading file '.white+datafile.green); 34 | 35 | try { 36 | // read the datafile. 37 | var s = fs.statSync(datafile); 38 | data = fs.readFileSync(datafile).toString(); 39 | } catch(err) { 40 | console.log("File doesn't exist!".red); 41 | console.log("You need a "+"data.json".yellow+" file in your "+dir.yellow+" folder."); 42 | process.exit(); 43 | } 44 | 45 | try { 46 | json = JSON.parse(data); 47 | } catch (e) { 48 | console.log("JSON isn\'t quite right in there.\n".red+ 49 | "Try pasting it in here: https://jsonformatter.curiousconcept.com/".red+ 50 | " and sort it out!"); 51 | process.exit(); 52 | } 53 | 54 | pages = json.pages; 55 | if (!json.browserWidth) json.browserWidth = 960; 56 | 57 | process.stdout.write('generating screenshots '.red); 58 | 59 | for (var i = 0; i < pages.length; i++) 60 | { 61 | if (typeof pages[i] == 'string') 62 | { 63 | var url = pages[i]; 64 | var file = pages[i].replace(/\//g,''); 65 | } else { 66 | var url = pages[i][0]; 67 | var file = pages[i][0].replace(/\//g,''), js = pages[i][1]; 68 | } 69 | 70 | var command = "webkit2png" 71 | + " -W "+json.browserWidth 72 | + " --clipwidth 1" 73 | + " -F "+json.webPath + url 74 | + " -o "+dir+'/'+file 75 | + " --ignore-ssl-check"; 76 | 77 | if (js) command = command + " --js='"+js+"'"; 78 | 79 | exec(command,function() 80 | { 81 | process.stdout.write('#'.white); 82 | // if we've done all of them set off the PDF creation. 83 | done++; 84 | if (done == pages.length) 85 | { 86 | process.stdout.write("\n"); 87 | createPDF(); 88 | } 89 | }); 90 | }; 91 | 92 | } else { 93 | 94 | // what they've passed isn't actually a folder. 95 | console.log('Hang on! That\'s not a folder.'.red); 96 | } 97 | 98 | } catch(err) { 99 | 100 | // if the folder doesn't exist. 101 | if (err.code == "ENOENT") console.log("Oops! Folder ".red+dir.white+" doesn\'t exist!".red); 102 | } 103 | 104 | /* 105 | ------------------- 106 | Function to create the PDF (honestly, just read the function name!) 107 | ------------------- 108 | */ 109 | function createPDF() 110 | { 111 | process.stdout.write('adding to PDF '.yellow); 112 | var page = { 113 | width:595, // A4 width (in pts at 72dpi) 114 | height:842, // A4 height 115 | margin:20 // margin! 116 | }; 117 | 118 | // create a document object 119 | var doc = new pdfdoc({size:[page.width,page.height]}); 120 | 121 | // adding them all to the PDF 122 | for (var i = 0; i < pages.length; i++) 123 | { 124 | if (typeof pages[i] == 'string') 125 | { 126 | var filename = pages[i].replace(/\//g,''); 127 | } else { 128 | var filename = pages[i][0].replace(/\//g,''); 129 | } 130 | var file = dir+'/'+filename+'-full.png'; 131 | 132 | process.stdout.write('#'.white); 133 | /* 134 | Documents start with a blank page so only add 135 | another for subsequent times. 136 | */ 137 | if (i > 0) doc.addPage(); 138 | /* 139 | Set up the margins and spacing. 140 | */ 141 | var xmarg = page.margin; 142 | var image_width = page.width-2*page.margin; 143 | /* 144 | If the width of the screen shot is less than 145 | the width of the page centre don't stretch it. 146 | */ 147 | if (json.browserWidth < image_width) 148 | { 149 | image_width = json.browserWidth; 150 | xmarg = (page.width - image_width)/2; 151 | } 152 | /* 153 | Add image to page. Also draw a grey rectangle around it. 154 | */ 155 | doc.image(file,xmarg,page.margin,{width:image_width}); 156 | doc.strokeColor("#eee"); 157 | doc.rect(xmarg,page.margin,image_width,page.height-page.margin).stroke(); 158 | 159 | /* 160 | Then delete the png file (as required). 161 | */ 162 | // 163 | if (!json.keepPNGs) fs.unlink(file); 164 | /* 165 | The create thumbnails (as required). 166 | */ 167 | if (json.keepPNGs && json.thumbWidth) { 168 | var thumbFile = file.replace('full','thumb'); 169 | fs.writeFileSync(thumbFile, fs.readFileSync(file)); 170 | } 171 | } 172 | 173 | /* 174 | Create the PDF file. 175 | */ 176 | doc.pipe(fs.createWriteStream(dir+"/"+json.output)); 177 | doc.end(); 178 | process.stdout.write("\n"); 179 | 180 | /* 181 | Then resize the images if (as required). 182 | */ 183 | if (json.thumbWidth && json.keepPNGs !== false) createThumbs(); 184 | else process.stdout.write("and now I'm done.\n".green); 185 | } 186 | 187 | function createThumbs() 188 | { 189 | var command = "mogrify -thumbnail "+json.thumbWidth+" "+dir+"/*-thumb.png"; 190 | process.stdout.write("resizing images ".magenta); 191 | exec(command,function() 192 | { 193 | process.stdout.write("done\n".white); 194 | process.stdout.write("and now I'm done.\n".green); 195 | }); 196 | } 197 | --------------------------------------------------------------------------------