feat: add Playwright long image downloads
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import fs from 'node:fs';
|
||||
import fsPromises from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
const DEFAULT_VIEWPORT_WIDTH = 1280;
|
||||
const DEFAULT_VIEWPORT_HEIGHT = 720;
|
||||
const MAX_LONG_IMAGE_HEIGHT = 20000;
|
||||
|
||||
export function isLongImageDownloadRequest(query) {
|
||||
const value = String(query?.download ?? query?.export ?? '').trim().toLowerCase();
|
||||
return value === 'long-image' || value === 'long_image' || value === 'png';
|
||||
}
|
||||
|
||||
export function longImagePathForHtml(htmlPath, outputPath = null) {
|
||||
return outputPath || String(htmlPath).replace(/\.html$/i, '.long.png');
|
||||
}
|
||||
|
||||
function clampDimension(value, fallback, max) {
|
||||
const number = Math.ceil(Number(value) || fallback);
|
||||
return Math.max(320, Math.min(number, max));
|
||||
}
|
||||
|
||||
async function launchChromium(chromium) {
|
||||
const base = {
|
||||
headless: true,
|
||||
args: ['--disable-dev-shm-usage', '--hide-scrollbars'],
|
||||
};
|
||||
if (process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH) {
|
||||
return chromium.launch({
|
||||
...base,
|
||||
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
|
||||
});
|
||||
}
|
||||
if (process.env.PLAYWRIGHT_CHROMIUM_CHANNEL) {
|
||||
return chromium.launch({ ...base, channel: process.env.PLAYWRIGHT_CHROMIUM_CHANNEL });
|
||||
}
|
||||
try {
|
||||
return await chromium.launch(base);
|
||||
} catch (error) {
|
||||
if (process.platform === 'darwin') {
|
||||
return chromium.launch({ ...base, channel: 'chrome' });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function renderLongImage({
|
||||
htmlPath = null,
|
||||
url = null,
|
||||
outputPath = null,
|
||||
viewportWidth = DEFAULT_VIEWPORT_WIDTH,
|
||||
viewportHeight = DEFAULT_VIEWPORT_HEIGHT,
|
||||
} = {}) {
|
||||
if (!htmlPath && !url) throw new Error('缺少 htmlPath 或 url');
|
||||
const targetUrl = url || pathToFileURL(path.resolve(htmlPath)).toString();
|
||||
const destination = outputPath ? path.resolve(outputPath) : longImagePathForHtml(path.resolve(htmlPath));
|
||||
const { chromium } = await import('playwright');
|
||||
let browser = null;
|
||||
try {
|
||||
browser = await launchChromium(chromium);
|
||||
const page = await browser.newPage({
|
||||
viewport: {
|
||||
width: clampDimension(viewportWidth, DEFAULT_VIEWPORT_WIDTH, 2400),
|
||||
height: clampDimension(viewportHeight, DEFAULT_VIEWPORT_HEIGHT, MAX_LONG_IMAGE_HEIGHT),
|
||||
},
|
||||
deviceScaleFactor: 2,
|
||||
});
|
||||
await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 30000 });
|
||||
await page.evaluate(() => document.fonts?.ready).catch(() => null);
|
||||
const size = await page.evaluate(() => ({
|
||||
width: Math.max(document.documentElement.scrollWidth, document.body.scrollWidth, 320),
|
||||
height: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight, 320),
|
||||
}));
|
||||
await page.setViewportSize({
|
||||
width: clampDimension(size.width, DEFAULT_VIEWPORT_WIDTH, 2400),
|
||||
height: Math.min(clampDimension(size.height, DEFAULT_VIEWPORT_HEIGHT, MAX_LONG_IMAGE_HEIGHT), 2400),
|
||||
});
|
||||
await fsPromises.mkdir(path.dirname(destination), { recursive: true });
|
||||
await page.screenshot({
|
||||
path: destination,
|
||||
fullPage: true,
|
||||
type: 'png',
|
||||
animations: 'disabled',
|
||||
caret: 'hide',
|
||||
});
|
||||
return {
|
||||
outputPath: destination,
|
||||
bytes: fs.statSync(destination).size,
|
||||
};
|
||||
} finally {
|
||||
await browser?.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
export async function renderLongImageBuffer({ url }) {
|
||||
const tempDir = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'memind-long-image-'));
|
||||
const outputPath = path.join(tempDir, 'page.long.png');
|
||||
try {
|
||||
await renderLongImage({ url, outputPath });
|
||||
return await fsPromises.readFile(outputPath);
|
||||
} finally {
|
||||
await fsPromises.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user