import puppeteer from 'puppeteer'; class ScreenshotService { constructor() { this.browser = null; this.isInitialized = false; } async initBrowser() { if (!this.browser) { console.log('初始化Puppeteer浏览器...'); this.browser = await puppeteer.launch({ headless: 'new', args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--no-first-run', '--no-zygote', '--single-process', '--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', '--disable-component-extensions-with-background-pages', '--force-device-scale-factor=1', '--enable-precise-memory-info', '--disable-features=VizDisplayCompositor', '--run-all-compositor-stages-before-draw', '--disable-new-content-rendering-timeout' ] }); console.log('Puppeteer浏览器初始化完成'); } return this.browser; } async closeBrowser() { if (this.browser) { await this.browser.close(); this.browser = null; console.log('Puppeteer浏览器已关闭'); } } // 从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]) }; } // 从CSS中提取 const cssMatch = htmlContent.match(/width:\s*(\d+)px.*height:\s*(\d+)px/); if (cssMatch) { return { width: parseInt(cssMatch[1]), height: parseInt(cssMatch[2]) }; } // 默认尺寸 return { width: 960, height: 720 }; } catch (error) { console.warn('提取PPT尺寸失败,使用默认尺寸:', error.message); return { width: 960, height: 720 }; } } // 优化HTML内容以确保精确截图 optimizeHtmlForScreenshot(htmlContent, targetWidth, targetHeight) { console.log(`优化HTML for精确截图, 目标尺寸: ${targetWidth}x${targetHeight}`); // 在标签后立即插入截图优化代码 const optimizedHtml = htmlContent.replace( /(]*>)/i, `$1 ` ); // 在前插入截图专用JavaScript const finalHtml = optimizedHtml.replace( /(<\/body>)/i, ` $1` ); console.log('HTML优化完成,已注入精确尺寸控制代码'); return finalHtml; } async generateScreenshot(htmlContent, options = {}) { try { await this.initBrowser(); console.log('开始生成截图...'); // 从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); // 创建新页面 const page = await this.browser.newPage(); try { // 设置精确的viewport尺寸 await page.setViewport({ width: dimensions.width, height: dimensions.height, deviceScaleFactor: options.deviceScaleFactor || 2, // 高清截图 }); // 设置页面内容 await page.setContent(optimizedHtml, { waitUntil: ['load', 'domcontentloaded', 'networkidle0'], timeout: 30000 }); // 等待页面完全渲染 await page.waitForTimeout(2000); // 执行最终的尺寸验证和强制设置 await page.evaluate((targetWidth, targetHeight) => { const html = document.documentElement; const body = document.body; const container = document.querySelector('.slide-container'); // 最终强制设置,确保精确尺寸 const forceSize = (element, width, height) => { if (!element) return; element.style.width = width + 'px'; element.style.height = height + 'px'; element.style.minWidth = width + 'px'; element.style.minHeight = height + 'px'; element.style.maxWidth = width + 'px'; element.style.maxHeight = height + 'px'; element.style.margin = '0'; element.style.padding = '0'; element.style.border = 'none'; element.style.outline = 'none'; element.style.overflow = 'hidden'; element.style.transform = 'none'; if (element !== container) { element.style.position = 'fixed'; element.style.top = '0'; element.style.left = '0'; } }; forceSize(html, targetWidth, targetHeight); forceSize(body, targetWidth, targetHeight); if (container) { forceSize(container, targetWidth, targetHeight); container.style.position = 'fixed'; container.style.top = '0'; container.style.left = '0'; container.style.boxShadow = 'none'; container.style.zIndex = '1'; } // 验证尺寸设置结果 const verification = { html: html.offsetWidth + 'x' + html.offsetHeight, body: body.offsetWidth + 'x' + body.offsetHeight, container: container ? container.offsetWidth + 'x' + container.offsetHeight : 'none', target: targetWidth + 'x' + targetHeight }; console.log('最终尺寸验证:', verification); // 如果尺寸不匹配,记录警告 if (html.offsetWidth !== targetWidth || html.offsetHeight !== targetHeight) { console.warn('HTML尺寸不匹配目标尺寸!'); } return verification; }, dimensions.width, dimensions.height); // 再次等待以确保所有更改生效 await page.waitForTimeout(1000); // 执行截图,使用精确的剪裁区域 const screenshot = await page.screenshot({ type: options.format || 'jpeg', quality: options.quality || 95, clip: { x: 0, y: 0, width: dimensions.width, height: dimensions.height }, omitBackground: false, // 包含背景 captureBeyondViewport: false, // 不截取视口外内容 }); console.log(`截图成功生成,尺寸: ${dimensions.width}x${dimensions.height}, 数据大小: ${screenshot.length} 字节`); return screenshot; } finally { await page.close(); } } catch (error) { console.error('截图生成失败:', error); throw new Error(`截图生成失败: ${error.message}`); } } // 兼容旧方法名 async captureScreenshot(htmlContent, width, height, options = {}) { console.log('使用兼容方法captureScreenshot,建议使用generateScreenshot'); return this.generateScreenshot(htmlContent, options); } } export default new ScreenshotService();