77 lines
3.7 KiB
JavaScript
Executable File
77 lines
3.7 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
||
/**
|
||
* Capture an overview using an available non-managed backend.
|
||
* Managed browser preview remains the preferred platform-level path; this CLI handles
|
||
* Firefox and deterministic SVG rasterization when those binaries are installed.
|
||
*/
|
||
import { execFileSync, spawnSync } from 'node:child_process';
|
||
import { existsSync, mkdirSync } from 'node:fs';
|
||
import { dirname, resolve } from 'node:path';
|
||
import { ScriptError, main, parseFlags } from './lib.js';
|
||
|
||
main(async () => {
|
||
const flags = parseFlags(process.argv.slice(2), {
|
||
required: ['output'],
|
||
optional: ['url', 'svg', 'backend', 'width', 'height'],
|
||
});
|
||
const output = resolve(flags.output);
|
||
mkdirSync(dirname(output), { recursive: true });
|
||
const backend = chooseBackend(flags.backend, flags.url, flags.svg);
|
||
if (backend === 'firefox') captureFirefox(flags.url, output, flags.width, flags.height);
|
||
else captureSvg(flags.svg, output, flags.width, flags.height);
|
||
return { backend, output };
|
||
});
|
||
|
||
function chooseBackend(requested, url, svg) {
|
||
if (requested && requested !== 'auto' && requested !== 'firefox' && requested !== 'svg') {
|
||
throw new ScriptError('BACKEND_UNKNOWN', `不支援的 capture backend:${requested}`);
|
||
}
|
||
if (requested === 'firefox') {
|
||
requireBinary('firefox');
|
||
if (!url) throw new ScriptError('URL_REQUIRED', 'Firefox backend 需要 --url');
|
||
return 'firefox';
|
||
}
|
||
if (requested === 'svg') {
|
||
requireSvgInput(svg);
|
||
requireAnyBinary(['rsvg-convert', 'resvg']);
|
||
return 'svg';
|
||
}
|
||
if (url && hasBinary('firefox')) return 'firefox';
|
||
if (svg && (hasBinary('rsvg-convert') || hasBinary('resvg'))) return 'svg';
|
||
throw new ScriptError('NO_CAPTURE_BACKEND', '沒有可用的 Firefox 或 SVG rasterizer;請使用平台 preview,或安裝 firefox、rsvg-convert、resvg');
|
||
}
|
||
|
||
function captureFirefox(url, output, width, height) {
|
||
const args = ['--headless'];
|
||
if (width) args.push('--window-size', `${width}${height ? `,${height}` : ''}`);
|
||
args.push('--screenshot', output, url);
|
||
const result = spawnSync('firefox', args, { encoding: 'utf8' });
|
||
if (result.status !== 0 || !existsSync(output)) throw new ScriptError('CAPTURE_FAILED', `Firefox 截圖失敗:${result.stderr || result.stdout || '沒有輸出檔案'}`);
|
||
}
|
||
|
||
function captureSvg(svg, output, width, height) {
|
||
requireSvgInput(svg);
|
||
if (hasBinary('rsvg-convert')) {
|
||
const args = [`--output=${output}`];
|
||
if (width) args.push(`--width=${width}`);
|
||
if (height) args.push(`--height=${height}`);
|
||
args.push(resolve(svg));
|
||
const result = spawnSync('rsvg-convert', args, { encoding: 'utf8' });
|
||
if (result.status !== 0) throw new ScriptError('CAPTURE_FAILED', `rsvg-convert 失敗:${result.stderr || result.stdout}`);
|
||
} else {
|
||
const args = [];
|
||
if (width) args.push('-w', String(width));
|
||
args.push(resolve(svg), output);
|
||
const result = spawnSync('resvg', args, { encoding: 'utf8' });
|
||
if (result.status !== 0) throw new ScriptError('CAPTURE_FAILED', `resvg 失敗:${result.stderr || result.stdout}`);
|
||
}
|
||
if (!existsSync(output)) throw new ScriptError('CAPTURE_FAILED', 'rasterizer 沒有產生輸出檔案');
|
||
}
|
||
|
||
function requireSvgInput(svg) {
|
||
if (!svg || !existsSync(resolve(svg))) throw new ScriptError('SVG_REQUIRED', 'SVG backend 需要存在的 --svg');
|
||
}
|
||
function requireBinary(binary) { if (!hasBinary(binary)) throw new ScriptError('BINARY_NOT_FOUND', `找不到 ${binary}`); }
|
||
function requireAnyBinary(binaries) { if (!binaries.some(hasBinary)) throw new ScriptError('BINARY_NOT_FOUND', `找不到 ${binaries.join(' 或 ')}`); }
|
||
function hasBinary(binary) { try { execFileSync('sh', ['-c', `command -v ${binary}`], { stdio: 'ignore' }); return true; } catch { return false; } }
|