Files
memind/mindspace-long-image.mjs
john 3de116d3e7
Memind CI / Test, build, and release guards (push) Successful in 1m26s
fix: hide platform actions from long image exports
2026-07-27 22:42:42 +08:00

146 lines
5.0 KiB
JavaScript

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