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 const LONG_IMAGE_EXPORT_STYLE = ` [data-mindspace-public-share], [data-mindspace-public-share-dialog], [data-mindspace-public-share-dialog-panel], .publication-share-fab, .publication-share-sheet { display: none !important; visibility: hidden !important; opacity: 0 !important; pointer-events: none !important; } `; export const LONG_IMAGE_EXPORT_SELECTORS = Object.freeze([ '[data-mindspace-public-share]', '[data-mindspace-public-share-dialog]', '[data-mindspace-public-share-dialog-panel]', '.publication-share-fab', '.publication-share-sheet', ]); 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 hideLongImageExportChrome(page) { await page.addStyleTag?.({ content: LONG_IMAGE_EXPORT_STYLE, }).catch(() => null); await page.evaluate?.((selectors) => { for (const selector of selectors) { for (const element of document.querySelectorAll(selector)) { element.setAttribute('data-memind-long-image-hidden', '1'); element.style.setProperty('display', 'none', 'important'); element.style.setProperty('visibility', 'hidden', 'important'); element.style.setProperty('opacity', '0', 'important'); element.style.setProperty('pointer-events', 'none', 'important'); } } }, LONG_IMAGE_EXPORT_SELECTORS).catch(() => null); } 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 hideLongImageExportChrome(page); await fsPromises.mkdir(path.dirname(destination), { recursive: true }); await page.screenshot({ path: destination, fullPage: true, type: 'png', animations: 'disabled', caret: 'hide', style: LONG_IMAGE_EXPORT_STYLE, }); 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(() => {}); } }