139 lines
5.4 KiB
JavaScript
139 lines
5.4 KiB
JavaScript
'use strict';
|
|
|
|
const puppeteer = require('puppeteer'),
|
|
{ AppInputError } = require('./app_error'),
|
|
debug = require('debug')('agm:web-util');
|
|
|
|
/**
|
|
* Take screenshot of a screen using a headless webdriver
|
|
* @param {*} params screenshot options { url: url, type: 'jpeg', 'png' (default), quality: 1-100 (75% default, jpeg only), width: number, height: number, path: path to save the output image }
|
|
*/
|
|
async function webShot(params, ops = { logTime: false, timeout: 30000 }) {
|
|
const logTime = !!(ops && ops.logTime);
|
|
if (logTime) console.time('webShot');
|
|
const type = params.type || 'png';
|
|
const quality = params.quality || 75;
|
|
const width = params.width || 800;
|
|
const height = params.height || 600;
|
|
|
|
if (!params || !params.url || !params.path) AppInputError.throw();
|
|
|
|
let browser;
|
|
try {
|
|
browser = await puppeteer.launch({
|
|
headless: 'new',
|
|
// headless: false,
|
|
slowMo: 250,
|
|
args: ['--incognito'],
|
|
ignoreHTTPSErrors: true,
|
|
ignoreDefaultArgs: ['--disable-dev-shm-usage'],
|
|
defaultViewport: { width: width, height: height },
|
|
fullPage: true
|
|
});
|
|
|
|
const pages = await browser.pages();
|
|
const page = pages.length ? pages[0] : await browser.newPage();
|
|
await page.goto(params.url);
|
|
// const selector = 'div.gm-style-cc a';
|
|
// await page.waitForFunction(selector => !!document.querySelector(selector), { timeout: 10000 }, selector);
|
|
// Generic wait condition when tiles all finished loading, the page set loaded can add some delay to make sure they all loaded visually perfect
|
|
await page.waitForFunction('window.loaded == true', { timeout: ops.timeout });
|
|
|
|
const shotOps = { type: type, clip: { x: 0, y: 0, width: width, height: height }, path: params.path };
|
|
if (type == 'jpeg') shotOps['quality'] = quality;
|
|
await page.screenshot(shotOps);
|
|
} catch (err) {
|
|
debug("input:", params);
|
|
throw err;
|
|
} finally {
|
|
if (browser) await browser.close();
|
|
if (logTime) console.timeEnd('webShot');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Capture multiple screenshots from ONE page in ONE Chromium instance (NFR-1.3 —
|
|
* the Advanced Report takes a mission map, thumbnails and zone maps per request;
|
|
* launching a browser per image the way webShot does would dominate the budget).
|
|
*
|
|
* @param {*} params { url, width, height } — page to load and viewport size
|
|
* @param {Array} shots executed in order; each is one of:
|
|
* { extract: '<js expression>' } -> push evaluated value into results
|
|
* { path, type?, quality?, clip?, clipExpr?, skipIf?, evaluate?, waitFor?, optional? } -> screenshot
|
|
* skipIf: page-side condition; when truthy the shot is skipped (null result) —
|
|
* lets one batch branch on page state (e.g. locator vs polygon mode)
|
|
* evaluate: JS to run before the shot (e.g. 'window.focusZone(2)')
|
|
* waitFor: condition to await before the shot (defaults to none)
|
|
* clipExpr: page-side expression returning {x, y, width, height} — clip computed
|
|
* by the page itself (e.g. a zone's pixel rect for thumbnail crops)
|
|
* optional: on failure push null and continue instead of throwing (NFR-3.1 —
|
|
* zone-map captures degrade, the mission map does not)
|
|
* @returns {Array} one entry per shot: saved path, extracted value, or null
|
|
*/
|
|
async function webShotBatch(params, shots, ops = { logTime: false, timeout: 30000 }) {
|
|
const logTime = !!(ops && ops.logTime);
|
|
if (logTime) console.time('webShotBatch');
|
|
if (!params || !params.url || !Array.isArray(shots)) AppInputError.throw();
|
|
const timeout = (ops && ops.timeout) || 30000;
|
|
const width = params.width || 800;
|
|
const height = params.height || 600;
|
|
|
|
let browser;
|
|
const results = [];
|
|
try {
|
|
browser = await puppeteer.launch({
|
|
headless: 'new',
|
|
args: ['--incognito'],
|
|
ignoreHTTPSErrors: true,
|
|
ignoreDefaultArgs: ['--disable-dev-shm-usage'],
|
|
defaultViewport: { width: width, height: height },
|
|
fullPage: true
|
|
});
|
|
|
|
const pages = await browser.pages();
|
|
const page = pages.length ? pages[0] : await browser.newPage();
|
|
await page.goto(params.url);
|
|
await page.waitForFunction('window.loaded == true', { timeout });
|
|
|
|
for (const shot of shots) {
|
|
try {
|
|
if (shot.extract !== undefined) {
|
|
results.push(await page.evaluate(shot.extract));
|
|
continue;
|
|
}
|
|
if (shot.skipIf && await page.evaluate(shot.skipIf)) {
|
|
results.push(null);
|
|
continue;
|
|
}
|
|
if (shot.evaluate) await page.evaluate(shot.evaluate);
|
|
if (shot.waitFor) await page.waitForFunction(shot.waitFor, { timeout });
|
|
|
|
const type = shot.type || 'jpeg';
|
|
const shotOps = {
|
|
type: type,
|
|
clip: shot.clip || (shot.clipExpr ? await page.evaluate(shot.clipExpr) : { x: 0, y: 0, width: width, height: height }),
|
|
path: shot.path
|
|
};
|
|
if (type == 'jpeg') shotOps['quality'] = shot.quality || 75;
|
|
await page.screenshot(shotOps);
|
|
results.push(shot.path);
|
|
} catch (err) {
|
|
if (!shot.optional) throw err;
|
|
debug('webShotBatch optional shot failed:', shot.path || shot.extract, err.message);
|
|
results.push(null);
|
|
}
|
|
}
|
|
return results;
|
|
} catch (err) {
|
|
debug("input:", params);
|
|
throw err;
|
|
} finally {
|
|
if (browser) await browser.close();
|
|
if (logTime) console.timeEnd('webShotBatch');
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
webShot,
|
|
webShotBatch,
|
|
} |