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