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(() => {}); } }