blob: 2ce36dc5188e6e841c7e72f6d94924e31d83d527 [file] [log] [blame]
const fs = require('fs');
const globby = require('globby');
const path = require('path');
const puppeteer = require('puppeteer');
const matter = require('gray-matter');
const argparse = require('argparse');
const minimatch = require('minimatch');
const { execFile } = require('child_process');
const cwebpBin = require('cwebp-bin');
const util = require('util');
const chalk = require('chalk');
const sharp = require('sharp');
const fse = require('fs-extra');
const { compareImage } = require('../common/compareImage');
const { runTasks } = require('../common/task');
const nStatic = require('node-static');
const shell = require('shelljs');
function optionToJson(obj, prop) {
let json = JSON.stringify(
obj,
function (key, value) {
if (typeof value === 'function') {
return 'expr: ' + value.toString();
}
return value;
},
2
);
return json;
}
function codeSize(code) {
return Buffer.byteLength(code, 'utf-8');
}
const parser = new argparse.ArgumentParser({
addHelp: true
});
parser.addArgument(['--gl'], {
help: 'If generating gl',
action: 'storeTrue'
});
parser.addArgument(['-t', '--theme'], {
help: 'Theme list, default to be all'
});
parser.addArgument(['-p', '--pattern'], {
help: 'Glob match patterns for generating thumb. https://github.com/isaacs/minimatch Mutiple match pattens can be splitted with ,'
});
parser.addArgument(['--no-thumb'], {
help: 'If not generate thumbs',
action: 'storeTrue'
});
const args = parser.parseArgs();
const isGL = args.gl;
let themeList = args.theme || 'default,dark';
let matchPattern = args.pattern;
if (matchPattern) {
matchPattern = matchPattern.split(',');
}
themeList = themeList.split(',');
function waitTime(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
const BUILD_THUMBS = !args.no_thumb;
const DEFAULT_PAGE_WIDTH = 700;
const DEFAULT_PAGE_RATIO = 0.75;
const OUTPUT_IMAGE_WIDTH = 600;
const OUTPUT_IMAGE_HEIGHT = OUTPUT_IMAGE_WIDTH * DEFAULT_PAGE_RATIO;
const PORT = 3323;
const BASE_URL = `http://localhost:${PORT}`;
const SCREENSHOT_PAGE_URL = `${BASE_URL}/tool/screenshot.html`;
const IGNORE_LOG = [
// For BMap
'A cookie associated with a cross-site resource at',
'A parser-blocking, cross site',
// For ECharts GL
'RENDER WARNING',
'GL ERROR',
'GL_INVALID_OPERATION'
];
function checkHasVideo(videoStart, videoEnd) {
return !isNaN(videoStart) && !isNaN(videoEnd) && +videoEnd > +videoStart;
}
async function convertToWebP(filePath) {
return util.promisify(execFile)(cwebpBin, [
filePath,
'-o',
filePath.replace(/\.png$/, '.webp')
]);
}
async function takeScreenshot(
browser,
ffmpeg,
theme,
rootDir,
basename,
hasVideo,
// Shot parameters
{ shotWidth, shotDelay, videoStart, videoEnd }
) {
const thumbFolder = theme !== 'default' ? 'thumb-' + theme : 'thumb';
const page = await browser.newPage();
const dataDir = isGL ? 'data-gl' : 'data';
const thumbDir = `${rootDir}public/${dataDir}/${thumbFolder}`;
const fileBase = `${thumbDir}/${basename}`;
const webmFile = `${fileBase}.webm`;
function checkDownloadFile() {
return new Promise((resolve) => {
let timeout = 0;
function check() {
if (fs.existsSync(webmFile)) {
resolve();
return;
}
timeout += 100;
if (timeout >= 20000 + +videoEnd) {
console.error(fileBase + '.webm download timeout.');
resolve();
return;
}
setTimeout(check, 100);
}
setTimeout(check, 100);
});
}
let checkingDownload;
await page.setViewport({
width: shotWidth || DEFAULT_PAGE_WIDTH,
height: (shotWidth || DEFAULT_PAGE_WIDTH) * DEFAULT_PAGE_RATIO
});
let url = `${SCREENSHOT_PAGE_URL}?c=${basename}&t=${theme}${
isGL ? '&gl' : ''
}`;
if (hasVideo) {
url += `&start=${videoStart}&end=${videoEnd}`;
await page._client.send('Page.setDownloadBehavior', {
behavior: 'allow',
downloadPath: thumbDir
});
checkingDownload = checkDownloadFile();
}
const resourceRootPath = `${BASE_URL}/public`;
// console.log(url);
await page.evaluateOnNewDocument(function (resourceRootPath) {
window.ROOT_PATH = resourceRootPath;
}, resourceRootPath);
page.on('pageerror', function (err) {
console.error(chalk.red('[pageerror in]', url));
console.error(chalk.red(err.toString()));
});
page.on('console', (msg) => {
const text = msg.text();
if (!IGNORE_LOG.find((a) => text.indexOf(a) >= 0)) {
console.log(chalk.gray(`PAGE LOG[${basename}]: ${text}`));
}
});
console.log(`Generating ${theme} thumbs.....${basename}`);
// https://stackoverflow.com/questions/46160929/puppeteer-wait-for-all-images-to-load-then-take-screenshot
try {
try {
await page.goto(url, {
waitUntil: 'networkidle0',
timeout: 20000
});
} catch (e) {
console.error(chalk.red(e));
// Timeout
}
await waitTime(200);
await waitTime(shotDelay || 0);
const filePathTmpRaw = `${fileBase}-tmp-raw.png`;
const filePathTmp = `${fileBase}-tmp.png`;
const filePath = `${fileBase}.png`;
fse.ensureDirSync(thumbDir);
// Save option for further tests.
try {
const option = await page.evaluate(() => {
return _$getEChartsOption();
});
const optionStr = optionToJson(option);
fse.ensureDirSync(`${rootDir}public/${dataDir}/option/`);
fs.writeFileSync(
`${rootDir}public/${dataDir}/option/${basename}.json`,
optionStr,
'utf-8'
);
} catch (e) {
console.error(chalk.red('Failed to generate option'));
console.error(chalk.red(e));
}
await page.screenshot({
path: filePathTmpRaw,
type: 'png'
});
await sharp(filePathTmpRaw)
.resize(OUTPUT_IMAGE_WIDTH, OUTPUT_IMAGE_HEIGHT)
.toFile(filePathTmp);
const { diffRatio } = await compareImage(filePath, filePathTmp, 0.1);
console.log(filePath);
if (diffRatio < 0.01) {
console.log('Not changed');
} else {
console.log(diffRatio);
fs.copyFileSync(filePathTmp, filePath);
if (!hasVideo) {
await convertToWebP(filePath);
}
}
try {
fs.unlinkSync(filePathOld);
} catch (e) {}
fs.unlinkSync(filePathTmpRaw);
fs.unlinkSync(filePathTmp);
if (hasVideo) {
await checkingDownload;
// const webpFile = `${fileBase}.webp`;
// const fileContent = fs.readFileSync(webmFile);
// ffmpeg.FS('writeFile', `${basename}.webm`, await fetchFile(fileContent));
// await ffmpeg.run('-i', `${basename}.webm`, '-f', 'webp', '-s', `${OUTPUT_IMAGE_WIDTH}x${OUTPUT_IMAGE_HEIGHT}`, `${basename}.webp`);
// fs.writeFileSync(webpFile, ffmpeg.FS('readFile', `${basename}.webp`));
// ffmpeg.FS("unlink", `${basename}.webm`)
// ffmpeg.FS("unlink", `${basename}.webp`)
shell.exec(
`ffmpeg -y -i ${fileBase}.webm -s ${OUTPUT_IMAGE_WIDTH}x${OUTPUT_IMAGE_HEIGHT} -f webp ${fileBase}.webp`
);
try {
fs.unlinkSync(webmFile);
} catch (e) {}
}
} catch (e) {
console.error(url);
console.error(e.toString());
}
await page.close();
}
(async () => {
const rootDir = path.join(__dirname, '../');
// TODO puppeteer will have Navigation Timeout Exceeded: 30000ms exceeded error in these examples.
const screenshotBlackList = [];
const examplesRoot = `${rootDir}public/examples`;
const files = await globby(`${examplesRoot}/js/${isGL ? 'gl/' : ''}*.js`);
const exampleList = [];
let thumbTasks = [];
for (let theme of themeList) {
for (let fileName of files) {
const basename = path.basename(fileName, '.js');
// Remove mapbox temporary
if (
basename.indexOf('mapbox') >= 0 ||
basename.indexOf('shanghai') >= 0 ||
basename === 'lines3d-taxi-routes-of-cape-town' ||
basename === 'lines3d-taxi-chengdu' ||
basename === 'map3d-colorful-cities' ||
// TODO Examples that can't work temporary.
basename === 'bar3d-music-visualization'
) {
continue;
}
const tsFile = `${examplesRoot}/ts/${isGL ? 'gl/' : ''}${basename}.ts`;
const hasTs = fs.existsSync(tsFile);
let fmResult;
try {
const code = fs.readFileSync(fileName, 'utf-8');
fmResult = matter(code, {
delimiters: ['/*', '*/']
});
} catch (e) {
fmResult = {
data: {}
};
}
try {
const difficulty =
fmResult.data.difficulty != null ? fmResult.data.difficulty : 10;
const category = (fmResult.data.category || '')
.split(/,/g)
.map((a) => a.trim())
.filter((a) => !!a);
if (!exampleList.find((item) => item.id === basename)) {
// Avoid add mulitple times when has multiple themes.
exampleList.push({
category: category,
id: basename,
ts: hasTs,
tags: (fmResult.data.tags || '')
.split(/,/g)
.map((a) => a.trim())
.filter((a) => !!a),
theme: fmResult.data.theme,
title: fmResult.data.title,
titleCN: fmResult.data.titleCN,
difficulty: +difficulty
});
}
} catch (e) {
throw new Error(e.toString());
}
if (
!matchPattern ||
(matchPattern.some(function (pattern) {
return minimatch(basename, pattern);
}) &&
screenshotBlackList.indexOf(basename) < 0)
) {
thumbTasks.push({
theme,
fmResult,
basename
});
}
}
}
exampleList.sort(function (a, b) {
if (a.difficulty === b.difficulty) {
return a.id.localeCompare(b.id);
}
return a.difficulty - b.difficulty;
});
const code = `
/* eslint-disable */
// THIS FILE IS GENERATED, DON'T MODIFY */
export default ${JSON.stringify(exampleList, null, 2)}`;
if (!matchPattern) {
fs.writeFileSync(
path.join(
__dirname,
`../src/data/chart-list-data${isGL ? '-gl' : ''}.js`
),
code,
'utf-8'
);
}
// Do screenshot
if (BUILD_THUMBS) {
const fileServer = new nStatic.Server(rootDir);
const server =
BUILD_THUMBS &&
require('http').createServer(function (request, response) {
request
.addListener('end', function () {
fileServer.serve(request, response);
})
.resume();
});
server && server.listen(PORT);
const browser = await puppeteer.launch({
headless: false,
args: [
'--headless',
'--hide-scrollbars',
// https://github.com/puppeteer/puppeteer/issues/4913
'--use-gl=egl',
'--mute-audio'
]
});
let ffmpeg;
// const ffmpeg = createFFmpeg({ log: true });
// await ffmpeg.load();
try {
// Take screenshots
const animationTasks = thumbTasks.filter((task) => {
return checkHasVideo(
task.fmResult.data.videoStart,
task.fmResult.data.videoEnd
);
});
const staticTasks = thumbTasks.filter((task) => {
return !checkHasVideo(
task.fmResult.data.videoStart,
task.fmResult.data.videoEnd
);
});
await runTasks(
staticTasks,
async ({ basename, fmResult, theme }) => {
await takeScreenshot(
browser,
ffmpeg,
theme,
rootDir,
basename,
false,
{
shotWidth: fmResult.data.shotWidth,
shotDelay: fmResult.data.shotDelay
}
);
},
isGL ? 2 : 16
);
await runTasks(
animationTasks,
async ({ basename, fmResult, theme }) => {
await takeScreenshot(
browser,
ffmpeg,
theme,
rootDir,
basename,
true,
{
shotWidth: fmResult.data.shotWidth,
shotDelay: fmResult.data.shotDelay,
videoStart: fmResult.data.videoStart,
videoEnd: fmResult.data.videoEnd
}
);
},
1
); // Webm download seems has issue used with multithreads
} catch (e) {
server.close();
await browser.close();
throw new Error(e.toString());
}
server.close();
await browser.close();
// ffmpeg.exit(0);
}
})();
process.on('SIGINT', function () {
console.log('Closing');
server.close();
// Close through ctrl + c;
process.exit();
});