94 lines
3.7 KiB
JavaScript
94 lines
3.7 KiB
JavaScript
import { app, BrowserWindow, shell } from "electron";
|
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { performance } from "node:perf_hooks";
|
|
import { buildCaptureDocument, writeCaptureWitness } from "../src/capture-model.mjs";
|
|
import { createEveElectronWindow } from "../src/window-host.mjs";
|
|
|
|
const options = parseArguments(process.argv.slice(2));
|
|
const tracePath = `${options.witness}.trace.log`;
|
|
mkdirSync(path.dirname(tracePath), { recursive: true });
|
|
trace("starting");
|
|
process.on("uncaughtException", error => fail(error));
|
|
process.on("unhandledRejection", error => fail(error));
|
|
app.on("ready", () => trace("ready-event"));
|
|
app.on("will-quit", () => trace("will-quit"));
|
|
void app.whenReady().then(runCapture, fail);
|
|
|
|
async function runCapture() {
|
|
trace("electron-ready");
|
|
const started = performance.now();
|
|
let window;
|
|
try {
|
|
const advertisement = parseJson(readFileSync(options.advertisement, "utf8"));
|
|
const surface = parseJson(readFileSync(options.surface, "utf8"));
|
|
const { projection, html } = buildCaptureDocument(advertisement, surface, options.surfaceId);
|
|
trace("projection-built");
|
|
mkdirSync(path.dirname(options.output), { recursive: true });
|
|
window = createEveElectronWindow({
|
|
width: 1280,
|
|
height: 800,
|
|
show: false,
|
|
frame: true,
|
|
title: `EveElectron ${projection.surfaceId}`,
|
|
webPreferences: { offscreen: true },
|
|
}, { BrowserWindow, shell });
|
|
const imagePromise = nextPaint(window, 10000);
|
|
await window.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
|
|
trace("window-loaded");
|
|
await window.webContents.executeJavaScript("document.fonts.ready.then(() => true)");
|
|
window.webContents.invalidate();
|
|
const image = await imagePromise;
|
|
trace("page-captured");
|
|
if (image.isEmpty()) throw new Error("Electron capture produced an empty native image.");
|
|
const png = image.toPNG();
|
|
if (png.length < 1024) throw new Error(`Electron capture is suspiciously small: ${png.length} bytes.`);
|
|
writeFileSync(options.output, png);
|
|
const size = image.getSize();
|
|
writeCaptureWitness({
|
|
outputPath: options.witness,
|
|
imagePath: options.output,
|
|
imageSize: size,
|
|
durationMs: Math.round((performance.now() - started) * 1000) / 1000,
|
|
advertisement,
|
|
surface,
|
|
projection,
|
|
});
|
|
console.log(options.witness);
|
|
trace("complete");
|
|
} finally {
|
|
window?.destroy();
|
|
app.exit(0);
|
|
}
|
|
}
|
|
|
|
function parseArguments(args) {
|
|
const values = {};
|
|
for (let index = 0; index < args.length; index += 2) values[args[index].slice(2)] = path.resolve(args[index + 1]);
|
|
for (const field of ["advertisement", "surface", "surface-id", "output", "witness"]) {
|
|
if (!values[field]) throw new Error(`Missing --${field}.`);
|
|
}
|
|
return { ...values, surfaceId: args[args.indexOf("--surface-id") + 1] };
|
|
}
|
|
|
|
function parseJson(value) { return JSON.parse(value.replace(/^\uFEFF/, "")); }
|
|
function trace(value) { writeFileSync(tracePath, `${new Date().toISOString()} ${value}\n`, { encoding: "utf8", flag: "a" }); }
|
|
function nextPaint(window, timeoutMs) {
|
|
return new Promise((resolve, reject) => {
|
|
const onPaint = (_event, _dirty, image) => {
|
|
if (image.isEmpty() || image.toPNG().length < 1024) return;
|
|
clearTimeout(timer);
|
|
window.webContents.off("paint", onPaint);
|
|
resolve(image);
|
|
};
|
|
const timer = setTimeout(() => {
|
|
window.webContents.off("paint", onPaint);
|
|
reject(new Error(`Electron offscreen paint timed out after ${timeoutMs}ms.`));
|
|
}, timeoutMs);
|
|
window.webContents.on("paint", onPaint);
|
|
});
|
|
}
|
|
function fail(error) {
|
|
trace(`failed ${error instanceof Error ? error.stack || error.message : String(error)}`);
|
|
app.exit(1);
|
|
}
|