web_ppt / backend /src /services /screenshotService.js
CatPtain's picture
Upload screenshotService.js
cf5bd9a verified
raw
history blame
14.8 kB
import puppeteer from 'puppeteer';
class ScreenshotService {
constructor() {
this.browser = null;
this.isInitialized = false;
this.maxRetries = 2;
this.browserLaunchRetries = 0;
this.maxBrowserLaunchRetries = 2;
this.isClosing = false;
this.isHuggingFaceSpace = process.env.SPACE_ID || process.env.HF_SPACE_ID;
}
async initBrowser() {
// 在Hugging Face Space环境中,Puppeteer可能不稳定,尝试更保守的配置
if (this.isClosing) {
console.log('浏览器正在关闭中,等待重新初始化...');
this.browser = null;
this.isClosing = false;
}
if (!this.browser) {
try {
console.log('初始化Puppeteer浏览器...');
const launchOptions = {
headless: 'new',
timeout: 60000,
protocolTimeout: 60000,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--disable-gpu',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-features=TranslateUI',
'--disable-extensions',
'--hide-scrollbars',
'--mute-audio',
'--no-default-browser-check',
'--disable-default-apps',
'--disable-background-networking',
'--disable-sync',
'--metrics-recording-only',
'--disable-domain-reliability',
'--force-device-scale-factor=1',
'--disable-features=VizDisplayCompositor',
'--run-all-compositor-stages-before-draw',
'--disable-new-content-rendering-timeout',
'--disable-ipc-flooding-protection',
'--disable-hang-monitor',
'--disable-prompt-on-repost',
'--memory-pressure-off',
'--max_old_space_size=1024'
]
};
// 如果是Hugging Face Space环境,使用更保守的配置
if (this.isHuggingFaceSpace) {
console.log('检测到Hugging Face Space环境,使用保守配置');
launchOptions.args.push(
'--single-process',
'--no-zygote',
'--disable-web-security',
'--disable-features=site-per-process',
'--disable-site-isolation-trials'
);
}
this.browser = await puppeteer.launch(launchOptions);
// 监听浏览器断开连接事件
this.browser.on('disconnected', () => {
console.log('Puppeteer浏览器连接断开');
this.browser = null;
this.isClosing = false;
});
console.log('Puppeteer浏览器初始化完成');
this.browserLaunchRetries = 0;
} catch (error) {
console.error('Puppeteer浏览器初始化失败:', error);
this.browserLaunchRetries++;
if (this.browserLaunchRetries <= this.maxBrowserLaunchRetries) {
console.log(`尝试重新初始化浏览器 (${this.browserLaunchRetries}/${this.maxBrowserLaunchRetries})`);
await this.delay(3000); // 等待3秒后重试
return this.initBrowser();
} else {
// 如果Puppeteer完全失败,返回null,使用fallback方法
console.warn('Puppeteer初始化完全失败,将使用fallback方法');
return null;
}
}
}
// 验证浏览器是否仍然连接
if (this.browser) {
try {
await this.browser.version();
} catch (error) {
console.warn('浏览器连接已断开,重新初始化:', error.message);
this.browser = null;
return this.initBrowser();
}
}
return this.browser;
}
async closeBrowser() {
if (this.browser && !this.isClosing) {
try {
this.isClosing = true;
console.log('正在关闭Puppeteer浏览器...');
await this.browser.close();
console.log('Puppeteer浏览器已关闭');
} catch (error) {
console.warn('关闭浏览器时出错:', error.message);
} finally {
this.browser = null;
this.isClosing = false;
}
}
}
// 延迟函数
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 从HTML中提取PPT尺寸信息
extractPPTDimensions(htmlContent) {
try {
// 从JavaScript中提取PPT_DIMENSIONS
const dimensionMatch = htmlContent.match(/window\.PPT_DIMENSIONS\s*=\s*{\s*width:\s*(\d+),\s*height:\s*(\d+)\s*}/);
if (dimensionMatch) {
return {
width: parseInt(dimensionMatch[1]),
height: parseInt(dimensionMatch[2])
};
}
// 从viewport meta标签中提取
const viewportMatch = htmlContent.match(/width=(\d+),\s*height=(\d+)/);
if (viewportMatch) {
return {
width: parseInt(viewportMatch[1]),
height: parseInt(viewportMatch[2])
};
}
// 默认尺寸
return { width: 960, height: 720 };
} catch (error) {
console.warn('提取PPT尺寸失败,使用默认尺寸:', error.message);
return { width: 960, height: 720 };
}
}
// 生成简化的PNG图片作为fallback
generateFallbackImage(width, height, message = 'Screenshot not available') {
// 创建一个简单的SVG作为fallback
const svg = `
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f0f0f0"/>
<text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle"
font-family="Arial, sans-serif" font-size="24" fill="#666">
${message}
</text>
<text x="50%" y="60%" text-anchor="middle" dominant-baseline="middle"
font-family="Arial, sans-serif" font-size="16" fill="#999">
Size: ${width}x${height}
</text>
</svg>
`;
// 将SVG转换为base64编码的PNG
const base64Svg = Buffer.from(svg).toString('base64');
const dataUrl = `data:image/svg+xml;base64,${base64Svg}`;
// 返回简单的占位图片数据
return Buffer.from(svg, 'utf8');
}
// 优化HTML内容以确保精确截图
optimizeHtmlForScreenshot(htmlContent, targetWidth, targetHeight) {
console.log(`优化HTML for精确截图, 目标尺寸: ${targetWidth}x${targetHeight}`);
// 在<head>标签后立即插入截图优化代码
const optimizedHtml = htmlContent.replace(
/(<head[^>]*>)/i,
`$1
<meta name="screenshot-mode" content="true">
<style id="screenshot-precise-control">
/* 截图模式:绝对精确的尺寸控制 */
*, *::before, *::after {
margin: 0 !important;
padding: 0 !important;
box-sizing: border-box !important;
border: none !important;
outline: none !important;
}
html {
width: ${targetWidth}px !important;
height: ${targetHeight}px !important;
min-width: ${targetWidth}px !important;
min-height: ${targetHeight}px !important;
max-width: ${targetWidth}px !important;
max-height: ${targetHeight}px !important;
overflow: hidden !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
margin: 0 !important;
padding: 0 !important;
transform: none !important;
transform-origin: top left !important;
}
body {
width: ${targetWidth}px !important;
height: ${targetHeight}px !important;
min-width: ${targetWidth}px !important;
min-height: ${targetHeight}px !important;
max-width: ${targetWidth}px !important;
max-height: ${targetHeight}px !important;
overflow: hidden !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
margin: 0 !important;
padding: 0 !important;
transform: none !important;
transform-origin: top left !important;
}
.slide-container {
width: ${targetWidth}px !important;
height: ${targetHeight}px !important;
min-width: ${targetWidth}px !important;
min-height: ${targetHeight}px !important;
max-width: ${targetWidth}px !important;
max-height: ${targetHeight}px !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
overflow: hidden !important;
transform: none !important;
transform-origin: top left !important;
margin: 0 !important;
padding: 0 !important;
box-shadow: none !important;
z-index: 1 !important;
}
/* 隐藏所有滚动条 */
html::-webkit-scrollbar,
body::-webkit-scrollbar,
*::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
}
html { scrollbar-width: none !important; }
/* 禁用用户交互 */
* {
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
user-select: none !important;
pointer-events: none !important;
}
</style>`
);
return optimizedHtml;
}
async generateScreenshotWithPuppeteer(htmlContent, options = {}, retryCount = 0) {
try {
const browser = await this.initBrowser();
if (!browser) {
throw new Error('浏览器初始化失败');
}
console.log(`开始生成截图... (Puppeteer尝试 ${retryCount + 1}/${this.maxRetries + 1})`);
// 从HTML中提取PPT的精确尺寸
const dimensions = this.extractPPTDimensions(htmlContent);
console.log(`检测到PPT尺寸: ${dimensions.width}x${dimensions.height}`);
// 优化HTML内容以确保精确截图
const optimizedHtml = this.optimizeHtmlForScreenshot(htmlContent, dimensions.width, dimensions.height);
// 创建新页面
let page = null;
try {
page = await browser.newPage();
// 设置页面超时
page.setDefaultTimeout(45000);
page.setDefaultNavigationTimeout(45000);
// 设置精确的viewport尺寸
await page.setViewport({
width: dimensions.width,
height: dimensions.height,
deviceScaleFactor: options.deviceScaleFactor || 1, // 降低设备缩放因子以减少内存使用
});
// 设置页面内容
await page.setContent(optimizedHtml, {
waitUntil: ['load', 'domcontentloaded'],
timeout: 45000
});
// 等待页面完全渲染
await page.waitForTimeout(1500);
// 执行截图,使用精确的剪裁区域
const screenshot = await page.screenshot({
type: options.format || 'jpeg',
quality: options.quality || 90,
clip: {
x: 0,
y: 0,
width: dimensions.width,
height: dimensions.height
},
omitBackground: false,
captureBeyondViewport: false,
});
console.log(`Puppeteer截图成功生成,尺寸: ${dimensions.width}x${dimensions.height}, 数据大小: ${screenshot.length} 字节`);
return screenshot;
} finally {
if (page) {
try {
await page.close();
} catch (error) {
console.warn('关闭页面时出错:', error.message);
}
}
}
} catch (error) {
console.error(`Puppeteer截图生成失败 (尝试 ${retryCount + 1}):`, error.message);
// 如果是目标关闭错误或连接断开,重置浏览器
if (error.message.includes('Target closed') ||
error.message.includes('Connection closed') ||
error.message.includes('Protocol error')) {
console.log('检测到浏览器连接问题,重置浏览器实例');
this.browser = null;
this.isClosing = false;
}
// 如果还有重试机会,进行重试
if (retryCount < this.maxRetries) {
console.log(`等待 ${(retryCount + 1) * 3} 秒后重试...`);
await this.delay((retryCount + 1) * 3000);
return this.generateScreenshotWithPuppeteer(htmlContent, options, retryCount + 1);
}
throw error;
}
}
async generateScreenshot(htmlContent, options = {}) {
try {
// 首先尝试使用Puppeteer
return await this.generateScreenshotWithPuppeteer(htmlContent, options);
} catch (puppeteerError) {
console.warn('Puppeteer截图失败,使用fallback方法:', puppeteerError.message);
// 如果Puppeteer失败,生成fallback图片
const dimensions = this.extractPPTDimensions(htmlContent);
const fallbackImage = this.generateFallbackImage(
dimensions.width,
dimensions.height,
'PPT Preview'
);
console.log(`生成fallback图片,尺寸: ${dimensions.width}x${dimensions.height}`);
return fallbackImage;
}
}
// 兼容旧方法名
async captureScreenshot(htmlContent, width, height, options = {}) {
console.log('使用兼容方法captureScreenshot,建议使用generateScreenshot');
return this.generateScreenshot(htmlContent, options);
}
// 清理资源
async cleanup() {
await this.closeBrowser();
}
}
// 创建单例实例
const screenshotService = new ScreenshotService();
// 进程退出时清理资源
process.on('exit', async () => {
await screenshotService.cleanup();
});
process.on('SIGINT', async () => {
await screenshotService.cleanup();
process.exit(0);
});
process.on('SIGTERM', async () => {
await screenshotService.cleanup();
process.exit(0);
});
export default screenshotService;