web_ppt / backend /src /services /screenshotService.js
CatPtain's picture
Upload screenshotService.js
03f0711 verified
import puppeteer from 'puppeteer';
import playwright from 'playwright';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import crypto from 'crypto';
// 获取当前文件的目录路径
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
class ScreenshotService {
constructor() {
this.puppeteerBrowser = null;
this.playwrightBrowser = null;
this.isInitializing = false;
this.puppeteerReady = false;
this.playwrightReady = false;
// 页面池 - 重用页面以提高性能
this.puppeteerPagePool = [];
this.playwrightPagePool = [];
this.maxPoolSize = 3; // 最大页面池大小
// 错误容忍和重启控制
this.healthCheckFailures = 0;
this.maxHealthCheckFailures = 3; // 连续失败3次才重启
this.lastRestartTime = 0;
this.minRestartInterval = 5 * 60 * 1000; // 最小重启间隔5分钟
// 主浏览器错误计数和故障状态
this.puppeteerErrorCount = 0;
this.maxPuppeteerErrors = 5; // 主浏览器连续错误5次后标记为故障
this.puppeteerFailed = false; // 主浏览器是否彻底故障
this.lastPuppeteerError = 0;
// 截图缓存 - 避免重复生成
this.screenshotCache = new Map();
this.maxCacheSize = 50; // 最大缓存数量
this.cacheExpireTime = 10 * 60 * 1000; // 缓存10分钟过期
// 字体配置
this.googleFonts = [
'Noto+Sans+SC:400,700',
'Noto+Serif+SC:400,700'
];
console.log('📸 Screenshot service initialized (optimized version)');
// 启动时预热浏览器
this.warmupBrowsers();
// 启动主浏览器恢复检查
this.startPrimaryBrowserRecovery();
}
// 预热浏览器实例 - 优先启动主浏览器
async warmupBrowsers() {
setTimeout(async () => {
try {
console.log('🔥 Warming up primary browser (Puppeteer)...');
await this.initPuppeteer();
if (this.puppeteerReady) {
console.log('✅ Primary browser warmup completed');
} else {
console.warn('⚠️ Primary browser failed to start, will use fallback when needed');
}
} catch (error) {
console.error('❌ Primary browser warmup failed:', error.message);
this.puppeteerFailed = true;
}
}, 1000); // 延迟1秒启动,避免与服务器启动冲突
}
// Initialize Puppeteer browser with optimizations
async initPuppeteer() {
if (this.puppeteerBrowser || this.isInitializing) return;
this.isInitializing = true;
try {
console.log('🚀 Starting Puppeteer browser (optimized)...');
// 优化的启动选项
const launchOptions = {
headless: 'new', // 使用新的无头模式
args: [
// 安全性设置
'--no-sandbox',
'--disable-setuid-sandbox',
// 内存和性能优化
'--disable-dev-shm-usage',
'--disable-extensions',
'--no-first-run',
'--no-default-browser-check',
'--disable-default-apps',
'--disable-features=TranslateUI',
'--disable-ipc-flooding-protection',
'--disable-features=VizDisplayCompositor',
'--disable-gpu-sandbox',
// 容器环境优化 - 禁用D-Bus和系统服务
'--disable-dbus',
'--disable-field-trial-config',
'--disable-translate',
'--disable-web-security',
'--allow-running-insecure-content',
// SVG和字体渲染优化
'--font-render-hinting=none', // 修复字体渲染不一致问题
'--enable-font-antialiasing', // 启用字体抗锯齿
'--force-color-profile=srgb', // 强制使用sRGB颜色配置
'--disable-software-rasterizer', // 禁用软件光栅化,使用硬件加速
'--enable-gpu-rasterization', // 启用GPU光栅化以改善SVG渲染
'--enable-oop-rasterization', // 启用进程外光栅化
'--disable-crash-reporter', // 禁用崩溃报告
'--disable-logging', // 禁用日志记录
// 内存限制优化
'--js-flags=--max-old-space-size=512', // 降低JS内存使用
'--memory-pressure-off',
// 进程优化
'--disable-background-networking',
'--disable-background-mode',
'--disable-renderer-backgrounding',
'--disable-backgrounding-occluded-windows',
'--disable-background-timer-throttling',
// 禁用不必要的功能
'--disable-breakpad',
'--disable-component-update',
'--disable-domain-reliability',
'--disable-sync',
'--disable-hang-monitor',
'--disable-prompt-on-repost',
'--disable-client-side-phishing-detection',
'--disable-component-extensions-with-background-pages',
'--blink-settings=imagesEnabled=true',
'--disable-gpu-process-crash-limit', // 禁用GPU进程崩溃限制
'--disable-features=VizDisplayCompositor,AudioServiceOutOfProcess', // 禁用可能导致崩溃的功能
'--no-zygote', // 禁用zygote进程
'--disable-accelerated-2d-canvas', // 禁用2D画布硬件加速
'--disable-accelerated-jpeg-decoding', // 禁用JPEG硬件解码
'--disable-accelerated-mjpeg-decode', // 禁用MJPEG硬件解码
'--disable-accelerated-video-decode' // 禁用视频硬件解码
],
timeout: 60000, // 增加超时时间
ignoreHTTPSErrors: true,
defaultViewport: null, // 动态设置视口
handleSIGINT: false, // 由我们自己处理
handleSIGTERM: false,
handleSIGHUP: false,
dumpio: false, // 禁用浏览器日志输出到控制台
protocolTimeout: 60000 // 增加协议超时时间,解决Network.enable超时问题
};
// 检测环境并应用特定优化
const isLowMemoryEnv = process.env.LOW_MEMORY === 'true';
if (isLowMemoryEnv) {
console.log('🔧 Applying low memory optimizations');
launchOptions.args.push(
'--disable-javascript',
'--disable-images',
'--disable-css-animations'
);
}
this.puppeteerBrowser = await puppeteer.launch(launchOptions);
this.puppeteerReady = true;
console.log('✅ Puppeteer browser started successfully');
// 预创建页面池
await this.initPagePool();
// 监控浏览器健康状态
this.startBrowserMonitoring();
// Cleanup when process exits
process.on('exit', () => this.cleanup());
process.on('SIGINT', () => this.cleanup());
process.on('SIGTERM', () => this.cleanup());
} catch (error) {
console.error('❌ Puppeteer initialization failed:', error.message);
this.puppeteerReady = false;
} finally {
this.isInitializing = false;
}
}
// 初始化页面池
async initPagePool() {
if (!this.puppeteerReady || !this.puppeteerBrowser) return;
try {
console.log(`🔄 Initializing page pool (size: ${this.maxPoolSize})...`);
// 清空现有池
for (const page of this.puppeteerPagePool) {
await page.close().catch(console.error);
}
this.puppeteerPagePool = [];
// 创建新页面
for (let i = 0; i < this.maxPoolSize; i++) {
const page = await this.puppeteerBrowser.newPage();
// 优化页面设置
await page.setRequestInterception(true);
page.on('request', (request) => {
// 优化的资源拦截逻辑,确保SVG相关资源能够正确加载
const resourceType = request.resourceType();
const url = request.url();
// 允许SVG相关资源和基本资源
if (resourceType === 'document' ||
resourceType === 'stylesheet' ||
resourceType === 'script' ||
resourceType === 'font' ||
url.includes('svg') ||
url.includes('data:image/svg') ||
resourceType === 'image') {
request.continue();
} else if (resourceType === 'media' && !url.includes('ads')) {
// 允许非广告媒体资源
request.continue();
} else {
// 阻止其他不必要的资源
request.abort();
}
});
// 注入Google字体
await this.injectGoogleFonts(page);
// 设置默认视口
await page.setViewport({ width: 1000, height: 562 });
// 添加到池中
this.puppeteerPagePool.push(page);
}
console.log(`✅ Page pool initialized with ${this.puppeteerPagePool.length} pages`);
} catch (error) {
console.error('❌ Failed to initialize page pool:', error.message);
}
}
// 注入Google字体
async injectGoogleFonts(page) {
try {
// 构建Google字体URL
const googleFontsUrl = `https://fonts.googleapis.com/css2?family=${this.googleFonts.join('&family=')}&display=swap`;
// 注入字体CSS(改进版)
await page.evaluateOnNewDocument((fontsUrl) => {
// 创建link元素加载Google字体
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = fontsUrl;
link.crossOrigin = 'anonymous'; // 添加跨域支持
// 立即添加到head,不等待DOMContentLoaded
const addFontLink = () => {
if (document.head) {
document.head.appendChild(link);
console.log('Google Fonts injected successfully');
} else {
// 如果head还不存在,等待一下再试
setTimeout(addFontLink, 10);
}
};
// 立即尝试添加
addFontLink();
// 同时添加备用字体CSS
const style = document.createElement('style');
style.textContent = `
* {
font-family: 'Noto Sans SC', 'Microsoft YaHei', 'SimHei', sans-serif !important;
}
svg text {
font-family: 'Noto Sans SC', 'Microsoft YaHei', 'SimHei', sans-serif !important;
}
`;
const addBackupStyle = () => {
if (document.head) {
document.head.appendChild(style);
} else {
setTimeout(addBackupStyle, 10);
}
};
addBackupStyle();
}, googleFontsUrl);
} catch (error) {
console.warn('⚠️ Failed to inject Google Fonts:', error.message);
}
}
// 监控浏览器健康状态
startBrowserMonitoring() {
const checkInterval = 15 * 60 * 1000; // 15分钟检查一次,减少检查频率
setInterval(async () => {
if (!this.puppeteerBrowser) return;
try {
// 检查浏览器是否响应
await this.puppeteerBrowser.version();
// 检查内存使用情况
const pages = await this.puppeteerBrowser.pages();
console.log(`🔍 Browser health check: ${pages.length} pages open`);
// 重置失败计数器
this.healthCheckFailures = 0;
// 如果页面过多,关闭多余页面(提高阈值)
if (pages.length > this.maxPoolSize * 4) { // 从2倍提高到4倍
console.log(`⚠️ Too many pages open (${pages.length}), cleaning up...`);
// 保留页面池中的页面,关闭其他页面
const poolPageIds = this.puppeteerPagePool.map(p => p.target()._targetId);
for (const page of pages) {
const pageId = page.target()._targetId;
if (!poolPageIds.includes(pageId)) {
await page.close().catch(console.error);
}
}
}
} catch (error) {
this.healthCheckFailures++;
console.error(`❌ Browser health check failed (${this.healthCheckFailures}/${this.maxHealthCheckFailures}):`, error.message);
// 只有连续失败多次且距离上次重启足够久才重启浏览器
if (this.healthCheckFailures >= this.maxHealthCheckFailures) {
const now = Date.now();
if (now - this.lastRestartTime > this.minRestartInterval) {
console.log('🔄 Multiple health check failures detected, attempting browser restart...');
await this.restartBrowser();
} else {
console.log('⏳ Browser restart skipped - too soon since last restart');
}
}
}
}, checkInterval);
}
// 重启浏览器 - 优先恢复主浏览器
async restartBrowser() {
console.log('🔄 Attempting to restart primary browser...');
try {
// 记录重启时间
this.lastRestartTime = Date.now();
// 清理现有资源
await this.cleanup();
// 重新初始化主浏览器
this.puppeteerReady = false;
this.puppeteerFailed = false;
this.puppeteerErrorCount = 0;
// 重置失败计数器
this.healthCheckFailures = 0;
await this.initPuppeteer();
if (this.puppeteerReady) {
console.log('✅ Primary browser successfully restarted');
// 如果主浏览器恢复,可以关闭副浏览器以节省资源
if (this.playwrightBrowser) {
console.log('🔄 Closing secondary browser to save resources');
await this.playwrightBrowser.close().catch(console.error);
this.playwrightBrowser = null;
this.playwrightReady = false;
this.playwrightPagePool = [];
}
} else {
console.warn('⚠️ Primary browser restart failed, marking as failed');
this.puppeteerFailed = true;
}
} catch (error) {
console.error('❌ Browser restart failed:', error.message);
this.puppeteerFailed = true;
// 重启失败时也要重置计数器,避免无限重试
this.healthCheckFailures = 0;
}
}
// Initialize Playwright browser (fallback) with optimizations
async initPlaywright() {
if (this.playwrightBrowser) return;
try {
console.log('🎭 Starting Playwright browser (optimized)...');
// 优化的启动选项
const launchOptions = {
headless: true,
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || '/usr/bin/chromium-browser',
args: [
// 安全性设置
'--no-sandbox',
'--disable-setuid-sandbox',
// 内存和性能优化
'--disable-dev-shm-usage',
'--disable-gpu',
'--disable-extensions',
'--js-flags=--max-old-space-size=512',
'--single-process',
'--disable-background-networking',
// 容器环境优化 - 禁用D-Bus和系统服务
'--disable-dbus',
'--disable-features=VizDisplayCompositor',
'--disable-software-rasterizer',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-field-trial-config',
'--disable-ipc-flooding-protection',
'--no-first-run',
'--no-default-browser-check',
'--disable-default-apps',
'--disable-component-update',
'--disable-sync',
'--disable-translate',
'--disable-web-security',
'--allow-running-insecure-content',
// 字体渲染优化
'--font-render-hinting=none'
],
ignoreDefaultArgs: ['--enable-automation'],
chromiumSandbox: false,
handleSIGINT: false,
handleSIGTERM: false,
handleSIGHUP: false,
timeout: 15000
};
console.log('🔧 Using Chromium executable:', launchOptions.executablePath);
this.playwrightBrowser = await playwright.chromium.launch(launchOptions);
// 初始化页面池
await this.initPlaywrightPagePool();
this.playwrightReady = true;
console.log('✅ Playwright browser started successfully');
} catch (error) {
console.error('❌ Playwright initialization failed:', error.message);
this.playwrightReady = false;
}
}
// 初始化Playwright页面池
async initPlaywrightPagePool() {
if (!this.playwrightBrowser) return;
try {
console.log(`🔄 Initializing Playwright page pool...`);
// 清空现有池
for (const page of this.playwrightPagePool) {
await page.close().catch(console.error);
}
this.playwrightPagePool = [];
// 创建新页面
for (let i = 0; i < this.maxPoolSize; i++) {
const context = await this.playwrightBrowser.newContext({
viewport: { width: 1000, height: 562 },
ignoreHTTPSErrors: true
});
const page = await context.newPage();
// 注入Google字体
await this.injectPlaywrightGoogleFonts(page);
// 添加到池中
this.playwrightPagePool.push(page);
}
console.log(`✅ Playwright page pool initialized with ${this.playwrightPagePool.length} pages`);
} catch (error) {
console.error('❌ Failed to initialize Playwright page pool:', error.message);
}
}
// 注入Google字体到Playwright页面
async injectPlaywrightGoogleFonts(page) {
try {
// 构建Google字体URL
const googleFontsUrl = `https://fonts.googleapis.com/css2?family=${this.googleFonts.join('&family=')}&display=swap`;
// 注入字体CSS
await page.addInitScript(({ fontsUrl }) => {
// 创建link元素加载Google字体
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = fontsUrl;
// 当文档创建时添加到head
document.addEventListener('DOMContentLoaded', () => {
document.head.appendChild(link);
});
}, { fontsUrl: googleFontsUrl });
} catch (error) {
console.warn('⚠️ Failed to inject Google Fonts to Playwright:', error.message);
}
}
// 生成缓存键
generateCacheKey(htmlContent, options) {
const { format, quality, width, height } = options;
// 使用内容和选项的哈希作为缓存键
const contentHash = crypto.createHash('md5').update(htmlContent).digest('hex').substring(0, 8);
return `${contentHash}_${width}x${height}_${format}_${quality}`;
}
// 清理过期缓存
cleanExpiredCache() {
const now = Date.now();
for (const [key, value] of this.screenshotCache.entries()) {
if (now - value.timestamp > this.cacheExpireTime) {
this.screenshotCache.delete(key);
}
}
// 如果缓存过多,删除最旧的
if (this.screenshotCache.size > this.maxCacheSize) {
const entries = Array.from(this.screenshotCache.entries());
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
const toDelete = entries.slice(0, entries.length - this.maxCacheSize);
toDelete.forEach(([key]) => this.screenshotCache.delete(key));
}
}
// 主浏览器恢复检查 - 定期尝试恢复故障的主浏览器
startPrimaryBrowserRecovery() {
const recoveryInterval = 30 * 60 * 1000; // 30分钟检查一次恢复
setInterval(async () => {
// 只有在主浏览器故障且副浏览器正在运行时才尝试恢复
if (this.puppeteerFailed && this.playwrightReady) {
const now = Date.now();
const timeSinceLastError = now - this.lastPuppeteerError;
// 距离上次错误至少10分钟后才尝试恢复
if (timeSinceLastError > 10 * 60 * 1000) {
console.log('🔄 Attempting to recover primary browser...');
try {
// 重置状态
this.puppeteerFailed = false;
this.puppeteerErrorCount = 0;
this.puppeteerReady = false;
// 尝试重新初始化主浏览器
await this.initPuppeteer();
if (this.puppeteerReady) {
console.log('✅ Primary browser successfully recovered!');
// 关闭副浏览器以节省资源
if (this.playwrightBrowser) {
console.log('🔄 Closing secondary browser after primary recovery');
await this.playwrightBrowser.close().catch(console.error);
this.playwrightBrowser = null;
this.playwrightReady = false;
this.playwrightPagePool = [];
}
} else {
console.warn('⚠️ Primary browser recovery failed, will retry later');
this.puppeteerFailed = true;
}
} catch (error) {
console.error('❌ Primary browser recovery error:', error.message);
this.puppeteerFailed = true;
}
}
}
}, recoveryInterval);
}
// Generate screenshot
async generateScreenshot(htmlContent, options = {}) {
const {
format = 'jpeg',
quality = 90,
width = 1000,
height = 562,
timeout = 30000 // 增加默认超时时间,与protocolTimeout保持一致
} = options;
// 检查缓存
const cacheKey = this.generateCacheKey(htmlContent, { format, quality, width, height });
const cached = this.screenshotCache.get(cacheKey);
if (cached && (Date.now() - cached.timestamp < this.cacheExpireTime)) {
console.log(`📋 Using cached screenshot: ${cacheKey}`);
return cached.data;
}
console.log(`📸 Generating screenshot: ${width}x${height}, ${format}@${quality}%`);
let screenshot = null;
// 优先使用主浏览器 (Puppeteer)
if (!this.puppeteerFailed) {
// 如果主浏览器未就绪,尝试重启(但不是每次都重启)
if (!this.puppeteerReady) {
const now = Date.now();
if (now - this.lastRestartTime > this.minRestartInterval) {
console.log('🔄 Primary browser not ready, attempting restart...');
await this.initPuppeteer();
}
}
if (this.puppeteerReady) {
try {
screenshot = await this.generateWithPuppeteer(htmlContent, { format, quality, width, height, timeout });
// 成功时重置错误计数
this.puppeteerErrorCount = 0;
} catch (error) {
this.puppeteerErrorCount++;
this.lastPuppeteerError = Date.now();
console.warn(`Puppeteer screenshot failed (${this.puppeteerErrorCount}/${this.maxPuppeteerErrors}):`, error.message);
// 如果错误次数过多,标记主浏览器为故障
if (this.puppeteerErrorCount >= this.maxPuppeteerErrors) {
console.error('❌ Primary browser marked as failed after multiple errors');
this.puppeteerFailed = true;
await this.cleanup(); // 清理故障的主浏览器
}
}
}
}
// 只有在主浏览器故障或截图失败时才启动副浏览器
if (!screenshot && (this.puppeteerFailed || this.puppeteerErrorCount > 0)) {
console.log('🔄 Falling back to secondary browser (Playwright)...');
if (!this.playwrightReady) {
await this.initPlaywright();
}
if (this.playwrightReady) {
try {
screenshot = await this.generateWithPlaywright(htmlContent, { format, quality, width, height, timeout });
} catch (error) {
console.warn('Playwright screenshot failed:', error.message);
}
}
}
// Final fallback: generate SVG
if (!screenshot) {
console.warn('All screenshot methods failed, generating fallback SVG');
screenshot = this.generateFallbackImage(width, height, 'Screenshot Service', 'Browser unavailable');
}
// 缓存结果(只缓存成功的截图,不缓存fallback)
if (screenshot && format !== 'svg') {
this.screenshotCache.set(cacheKey, {
data: screenshot,
timestamp: Date.now()
});
// 清理过期缓存
this.cleanExpiredCache();
console.log(`💾 Cached screenshot: ${cacheKey} (cache size: ${this.screenshotCache.size})`);
}
return screenshot;
}
// Generate screenshot using Puppeteer with page pool optimization
async generateWithPuppeteer(htmlContent, options) {
const { format, quality, width, height, timeout } = options;
let page = null;
let pageFromPool = false;
try {
// 尝试从页面池获取页面
if (this.puppeteerPagePool && this.puppeteerPagePool.length > 0) {
page = this.puppeteerPagePool.shift();
pageFromPool = true;
console.log(`📄 Using page from Puppeteer pool (${this.puppeteerPagePool.length} remaining)`);
} else {
// 如果池为空,创建新页面
page = await this.puppeteerBrowser.newPage();
console.log(`📄 Created new Puppeteer page (pool empty)`);
// 优化新页面设置
await page.setRequestInterception(true);
page.on('request', (request) => {
// 阻止不必要的资源加载
const resourceType = request.resourceType();
if (resourceType === 'media' || resourceType === 'font') {
// 允许字体资源加载
request.continue();
} else if (resourceType === 'stylesheet' || resourceType === 'script' || resourceType === 'image') {
// 允许基本资源
request.continue();
} else {
// 阻止其他资源
request.abort();
}
});
// 为新创建的页面注入Google字体
await this.injectGoogleFonts(page);
}
// 设置视口大小(即使从池中获取的页面也需要重新设置,因为尺寸可能不同)
await page.setViewport({ width, height });
// 设置内容,增加超时时间以解决Network.enable超时问题
await page.setContent(htmlContent, {
waitUntil: 'networkidle0',
timeout: Math.min(timeout, 20000) // 增加超时时间,解决网络连接超时问题
});
// 等待字体加载
await page.evaluate(() => {
return document.fonts ? document.fonts.ready : Promise.resolve();
});
// 等待SVG元素完全加载和渲染(改进版)
await page.evaluate(() => {
return new Promise(resolve => {
const svgs = document.querySelectorAll('svg');
if (svgs.length === 0) {
resolve();
return;
}
console.log(`Found ${svgs.length} SVG elements, waiting for rendering...`);
// 检查SVG是否已渲染的函数
const checkSVGRendered = (svg) => {
try {
// 检查SVG的bounding box是否有效
const bbox = svg.getBBox();
return bbox && bbox.width > 0 && bbox.height > 0;
} catch (e) {
// 如果getBBox失败,检查SVG是否有子元素
return svg.children.length > 0;
}
};
// 等待所有SVG渲染完成
const waitForSVGs = () => {
let allRendered = true;
svgs.forEach(svg => {
if (!checkSVGRendered(svg)) {
allRendered = false;
}
});
if (allRendered) {
console.log('All SVG elements rendered successfully');
resolve();
} else {
// 继续等待
setTimeout(waitForSVGs, 100);
}
};
// 开始检查
waitForSVGs();
// 超时保护,最多等待5秒
setTimeout(() => {
console.log('SVG rendering timeout, proceeding anyway');
resolve();
}, 5000);
});
});
// 额外等待时间确保渲染完成,特别是SVG元素
await page.waitForTimeout(1500);
// 优化内存使用
if (global.gc && Math.random() < 0.1) { // 随机触发GC,避免每次都执行
global.gc();
}
// 优化的截图选项,提高矢量图形质量
const screenshotOptions = {
type: format,
quality: format === 'jpeg' ? Math.max(quality, 90) : undefined, // 提高JPEG质量
fullPage: false,
omitBackground: format === 'png', // PNG格式可以透明背景
clip: { x: 0, y: 0, width, height },
// 添加设备像素比以提高清晰度,特别是对矢量图形
deviceScaleFactor: 2
};
const screenshot = await page.screenshot(screenshotOptions);
console.log(`✅ Puppeteer screenshot generated: ${screenshot.length} bytes`);
return screenshot;
} catch (error) {
console.error(`❌ Puppeteer screenshot failed: ${error.message}`);
throw error;
} finally {
if (page) {
if (pageFromPool && this.puppeteerPagePool.length < this.maxPoolSize) {
// 重置页面状态并放回池中
try {
// 清理页面状态,但不导航到about:blank(减少操作)
await page.evaluate(() => {
// 清理页面内容但保持基本结构
if (document.body) document.body.innerHTML = '';
if (document.head) {
// 保留基本的meta标签和字体链接
const links = document.head.querySelectorAll('link[rel="stylesheet"]');
const metas = document.head.querySelectorAll('meta');
document.head.innerHTML = '';
metas.forEach(meta => document.head.appendChild(meta));
links.forEach(link => document.head.appendChild(link));
}
});
// 将页面放回池中
this.puppeteerPagePool.push(page);
console.log(`♻️ Returned Puppeteer page to pool (${this.puppeteerPagePool.length} total)`);
} catch (e) {
console.warn('⚠️ Failed to reset page, closing it:', e.message);
await page.close().catch(console.error);
}
} else if (!pageFromPool) {
// 如果池已满,尝试将新页面也加入池中(替换最旧的)
if (this.puppeteerPagePool.length >= this.maxPoolSize) {
const oldPage = this.puppeteerPagePool.shift();
await oldPage.close().catch(console.error);
}
try {
// 重置新页面状态
await page.evaluate(() => {
if (document.body) document.body.innerHTML = '';
});
this.puppeteerPagePool.push(page);
console.log(`♻️ Added new page to pool (${this.puppeteerPagePool.length} total)`);
} catch (e) {
console.warn('⚠️ Failed to add page to pool, closing it:', e.message);
await page.close().catch(console.error);
}
}
}
}
}
// Generate screenshot using Playwright with page pool optimization
async generateWithPlaywright(htmlContent, options) {
const { format, quality, width, height, timeout } = options;
let page = null;
let pageFromPool = false;
try {
// 尝试从页面池获取页面
if (this.playwrightPagePool && this.playwrightPagePool.length > 0) {
page = this.playwrightPagePool.shift();
pageFromPool = true;
console.log(`📄 Using page from Playwright pool (${this.playwrightPagePool.length} remaining)`);
} else {
// 如果池为空,创建新页面
const context = await this.playwrightBrowser.newContext({
viewport: { width, height },
ignoreHTTPSErrors: true
});
page = await context.newPage();
console.log(`📄 Created new Playwright page (pool empty)`);
// 为新创建的页面注入Google字体
await this.injectPlaywrightGoogleFonts(page);
}
// 设置视口大小(即使从池中获取的页面也需要重新设置,因为尺寸可能不同)
await page.setViewportSize({ width, height });
// 设置内容,使用更短的超时时间
await page.setContent(htmlContent, {
waitUntil: 'networkidle',
timeout: Math.min(timeout, 10000) // 使用较短的超时时间
});
// 等待字体加载
await page.evaluate(() => {
return document.fonts ? document.fonts.ready : Promise.resolve();
});
// 减少等待时间
await page.waitForTimeout(200);
// 优化内存使用
if (global.gc && Math.random() < 0.1) { // 随机触发GC,避免每次都执行
global.gc();
}
// 截图选项
const screenshotOptions = {
type: format,
quality: format === 'jpeg' ? quality : undefined,
fullPage: false,
omitBackground: format === 'png', // PNG格式可以透明背景
clip: { x: 0, y: 0, width, height }
};
const screenshot = await page.screenshot(screenshotOptions);
console.log(`✅ Playwright screenshot generated: ${screenshot.length} bytes`);
return screenshot;
} catch (error) {
console.error(`❌ Playwright screenshot failed: ${error.message}`);
throw error;
} finally {
if (page) {
if (pageFromPool && this.playwrightPagePool.length < this.maxPoolSize) {
// 重置页面状态
try {
await page.goto('about:blank').catch(() => {});
// 将页面放回池中
this.playwrightPagePool.push(page);
console.log(`♻️ Returned Playwright page to pool (${this.playwrightPagePool.length} total)`);
} catch (e) {
console.warn('⚠️ Failed to reset page, closing it:', e.message);
await page.close().catch(console.error);
}
} else if (!pageFromPool) {
// 关闭非池中的页面
await page.close().catch(console.error);
}
}
}
}
// Generate fallback SVG image
generateFallbackImage(width = 1000, height = 562, title = 'PPT Screenshot', subtitle = '', message = '') {
const svg = `<?xml version="1.0" encoding="UTF-8"?>
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#f8f9fa;stop-opacity:1" />
<stop offset="100%" style="stop-color:#e9ecef;stop-opacity:1" />
</linearGradient>
<pattern id="dots" patternUnits="userSpaceOnUse" width="20" height="20">
<circle cx="2" cy="2" r="1" fill="#dee2e6" opacity="0.5"/>
</pattern>
</defs>
<!-- Background -->
<rect width="100%" height="100%" fill="url(#bg)"/>
<rect width="100%" height="100%" fill="url(#dots)"/>
<!-- Border -->
<rect x="20" y="20" width="${width-40}" height="${height-40}"
fill="none" stroke="#dee2e6" stroke-width="3"
stroke-dasharray="15,10" rx="10"/>
<!-- Icon -->
<g transform="translate(${width/2}, ${height*0.25})">
<circle cx="0" cy="0" r="30" fill="#6c757d" opacity="0.3"/>
<rect x="-15" y="-10" width="30" height="20" rx="3" fill="#6c757d"/>
<rect x="-12" y="-7" width="24" height="3" fill="white"/>
<rect x="-12" y="-2" width="24" height="3" fill="white"/>
<rect x="-12" y="3" width="16" height="3" fill="white"/>
</g>
<!-- Title -->
<text x="${width/2}" y="${height*0.45}"
text-anchor="middle"
font-family="Microsoft YaHei, PingFang SC, Hiragino Sans GB, Source Han Sans SC, Noto Sans SC, WenQuanYi Micro Hei, SimHei, SimSun, Arial, sans-serif"
font-size="28"
font-weight="bold"
fill="#495057">${title}</text>
${subtitle ? `
<!-- Subtitle -->
<text x="${width/2}" y="${height*0.55}"
text-anchor="middle"
font-family="Microsoft YaHei, PingFang SC, Hiragino Sans GB, Source Han Sans SC, Noto Sans SC, WenQuanYi Micro Hei, SimHei, SimSun, Arial, sans-serif"
font-size="18"
fill="#6c757d">${subtitle}</text>
` : ''}
${message ? `
<!-- Message -->
<text x="${width/2}" y="${height*0.65}"
text-anchor="middle"
font-family="Microsoft YaHei, PingFang SC, Hiragino Sans GB, Source Han Sans SC, Noto Sans SC, WenQuanYi Micro Hei, SimHei, SimSun, Arial, sans-serif"
font-size="14"
fill="#adb5bd">${message}</text>
` : ''}
<!-- Footer -->
<text x="${width/2}" y="${height*0.85}"
text-anchor="middle"
font-family="Microsoft YaHei, PingFang SC, Hiragino Sans GB, Source Han Sans SC, Noto Sans SC, WenQuanYi Micro Hei, SimHei, SimSun, Arial, sans-serif"
font-size="12"
fill="#ced4da">PPT Screenshot Service • ${new Date().toLocaleString('zh-CN')}</text>
<!-- Dimensions info -->
<text x="${width/2}" y="${height*0.92}"
text-anchor="middle"
font-family="Microsoft YaHei, PingFang SC, Hiragino Sans GB, Source Han Sans SC, Noto Sans SC, WenQuanYi Micro Hei, SimHei, SimSun, Arial, sans-serif"
font-size="10"
fill="#ced4da">Size: ${width} × ${height}</text>
</svg>`;
return Buffer.from(svg, 'utf-8');
}
// Get service status
getStatus() {
return {
puppeteerReady: this.puppeteerReady,
playwrightReady: this.playwrightReady,
environment: process.env.NODE_ENV || 'development',
isHuggingFace: !!process.env.SPACE_ID
};
}
// Cleanup resources
async cleanup() {
console.log('🧹 Cleaning up screenshot service...');
if (this.puppeteerBrowser) {
try {
await this.puppeteerBrowser.close();
this.puppeteerBrowser = null;
this.puppeteerReady = false;
console.log('✅ Puppeteer browser closed');
} catch (error) {
console.error('Error closing Puppeteer browser:', error);
}
}
if (this.playwrightBrowser) {
try {
await this.playwrightBrowser.close();
this.playwrightBrowser = null;
this.playwrightReady = false;
console.log('✅ Playwright browser closed');
} catch (error) {
console.error('Error closing Playwright browser:', error);
}
}
}
}
export default new ScreenshotService();