1/** 2 * Command line application to build a 5x5 filmstrip from a Lottie file in the 3 * browser and then exporting that filmstrip in a 1000x1000 PNG. 4 * 5 */ 6const puppeteer = require('puppeteer'); 7const express = require('express'); 8const fs = require('fs'); 9const commandLineArgs = require('command-line-args'); 10const commandLineUsage= require('command-line-usage'); 11const fetch = require('node-fetch'); 12 13// Valid values for the --renderer flag. 14const RENDERERS = ['svg', 'canvas']; 15 16const opts = [ 17 { 18 name: 'input', 19 typeLabel: '{underline file}', 20 description: 'The Lottie JSON file to process.' 21 }, 22 { 23 name: 'output', 24 typeLabel: '{underline file}', 25 description: 'The captured filmstrip PNG file to write. Defaults to filmstrip.png', 26 }, 27 { 28 name: 'renderer', 29 typeLabel: '{underline mode}', 30 description: 'Which renderer to use, "svg" or "canvas". Defaults to "svg".', 31 }, 32 { 33 name: 'port', 34 description: 'The port number to use, defaults to 8081.', 35 type: Number, 36 }, 37 { 38 name: 'lottie_player', 39 description: 'The path to lottie.min.js, defaults to a local npm install location.', 40 type: String, 41 }, 42 { 43 name: 'post_to', 44 description: 'If set, the url to post results to for Gold Ingestion.', 45 type: String, 46 }, 47 { 48 name: 'in_docker', 49 description: 'Is this being run in docker, defaults to false', 50 type: Boolean, 51 }, 52 { 53 name: 'skip_automation', 54 description: 'If the automation of the screenshot taking should be skipped ' + 55 '(e.g. debugging). Defaults to false.', 56 type: Boolean, 57 }, 58 { 59 name: 'help', 60 alias: 'h', 61 type: Boolean, 62 description: 'Print this usage guide.' 63 }, 64]; 65 66const usage = [ 67 { 68 header: 'Lottie Filmstrip Capture', 69 content: `Command line application to build a 5x5 filmstrip 70from a Lottie file in the browser and then export 71that filmstrip in a 1000x1000 PNG.` 72 }, 73 { 74 header: 'Options', 75 optionList: opts, 76 }, 77]; 78 79// Parse and validate flags. 80const options = commandLineArgs(opts); 81 82if (!options.output) { 83 options.output = 'filmstrip.png'; 84} 85if (!options.port) { 86 options.port = 8081; 87} 88if (!options.lottie_player) { 89 options.lottie_player = 'node_modules/lottie-web/build/player/lottie.min.js'; 90} 91 92if (options.help) { 93 console.log(commandLineUsage(usage)); 94 process.exit(0); 95} 96 97if (!options.input) { 98 console.error('You must supply a Lottie JSON filename.'); 99 console.log(commandLineUsage(usage)); 100 process.exit(1); 101} 102 103if (!options.renderer) { 104 options.renderer = 'svg'; 105} 106 107if (!RENDERERS.includes(options.renderer)) { 108 console.error('The --renderer flag must have as a value one of: ', RENDERERS); 109 console.log(commandLineUsage(usage)); 110 process.exit(1); 111} 112 113// Start up a web server to serve the three files we need. 114let lottieJS = fs.readFileSync(options.lottie_player, 'utf8'); 115let driverHTML = fs.readFileSync('driver.html', 'utf8'); 116let lottieJSON = fs.readFileSync(options.input, 'utf8'); 117 118const app = express(); 119app.get('/', (req, res) => res.send(driverHTML)); 120app.get('/lottie.js', (req, res) => res.send(lottieJS)); 121app.get('/lottie.json', (req, res) => res.send(lottieJSON)); 122app.listen(options.port, () => console.log('- Local web server started.')) 123 124// Utiltity function. 125async function wait(ms) { 126 await new Promise(resolve => setTimeout(() => resolve(), ms)); 127 return ms; 128} 129 130const targetURL = `http://localhost:${options.port}/#${options.renderer}`; 131 132// Drive chrome to load the web page from the server we have running. 133async function driveBrowser() { 134 console.log('- Launching chrome in headless mode.'); 135 let browser = null; 136 if (options.in_docker) { 137 browser = await puppeteer.launch({ 138 'executablePath': '/usr/bin/google-chrome-stable', 139 'args': ['--no-sandbox'], 140 }); 141 } else { 142 browser = await puppeteer.launch(); 143 } 144 145 const page = await browser.newPage(); 146 console.log(`- Loading our Lottie exercising page for ${options.input}.`); 147 try { 148 // 20 seconds is plenty of time to wait for the json to be loaded once 149 // This usually times out for super large json. 150 await page.goto(targetURL, { 151 timeout: 20000, 152 waitUntil: 'networkidle0' 153 }); 154 // 20 seconds is plenty of time to wait for the frames to be drawn. 155 // This usually times out for json that causes errors in the player. 156 console.log('- Waiting 15s for all the tiles to be drawn.'); 157 await page.waitForFunction('window._tileCount === 25', { 158 timeout: 20000, 159 }); 160 } catch(e) { 161 console.log('Timed out while loading or drawing. Either the JSON file was ' + 162 'too big or hit a bug in the player.', e); 163 await browser.close(); 164 process.exit(0); 165 } 166 167 console.log('- Taking screenshot.'); 168 let encoding = 'binary'; 169 if (options.post_to) { 170 encoding = 'base64'; 171 // prevent writing the image to disk 172 options.output = ''; 173 } 174 175 // See https://github.com/GoogleChrome/puppeteer/blob/v1.6.0/docs/api.md#pagescreenshotoptions 176 let result = await page.screenshot({ 177 path: options.output, 178 type: 'png', 179 clip: { 180 x: 0, 181 y: 0, 182 width: 1000, 183 height: 1000, 184 }, 185 encoding: encoding, 186 }); 187 188 if (options.post_to) { 189 console.log(`- Reporting ${options.input} to Gold server ${options.post_to}`); 190 let shortenedName = options.input; 191 let lastSlash = shortenedName.lastIndexOf('/'); 192 if (lastSlash !== -1) { 193 shortenedName = shortenedName.slice(lastSlash+1); 194 } 195 await fetch(options.post_to, { 196 method: 'POST', 197 mode: 'no-cors', 198 headers: { 199 'Content-Type': 'application/json', 200 }, 201 body: JSON.stringify({ 202 'data': result, 203 'test_name': shortenedName, 204 }) 205 }); 206 } 207 208 await browser.close(); 209 // Need to call exit() because the web server is still running. 210 process.exit(0); 211} 212 213if (!options.skip_automation) { 214 driveBrowser(); 215} else { 216 console.log(`open ${targetURL} to see the animation.`) 217} 218 219