web_ppt / backend /src /services /screenshotService.js
CatPtain's picture
Upload screenshotService.js
21da54e verified
raw
history blame
16.1 kB
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'
]
});
console.log('Puppeteer浏览器初始化完成');
}
return this.browser;
}
async closeBrowser() {
if (this.browser) {
await this.browser.close();
this.browser = null;
console.log('Puppeteer浏览器已关闭');
}
}
async modifyHtmlForScreenshot(htmlContent, targetWidth, targetHeight) {
console.log(`开始修改HTML for截图, 目标尺寸: ${targetWidth}x${targetHeight}`);
// 创建完全针对截图优化的HTML版本
const optimizedHtml = htmlContent.replace(
/<head>/i,
`<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=${targetWidth}, height=${targetHeight}, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover">
<!-- 截图模式标识 -->
<meta name="screenshot-mode" content="true">
<!-- 强制精确尺寸控制 -->
<style id="screenshot-optimization">
/* 全局重置 - 完全消除边距 */
*, *::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;
border: none !important;
outline: none !important;
transform: none !important;
transform-origin: top left !important;
}
/* Body元素强制设置 */
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;
border: none !important;
outline: none !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;
border: none !important;
outline: none !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;
}
/* Firefox */
html {
scrollbar-width: none !important;
}
/* 禁用用户交互 */
* {
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
user-select: none !important;
-webkit-user-drag: none !important;
-khtml-user-drag: none !important;
-moz-user-drag: none !important;
-o-user-drag: none !important;
user-drag: none !important;
pointer-events: none !important;
}
/* 移除响应式样式 */
.browse-mode * {
display: none !important;
}
</style>`
);
// 注入截图专用JavaScript
const finalHtml = optimizedHtml.replace(
/<\/body>/i,
`
<script id="screenshot-finalizer">
// 截图模式最终设置
window.SCREENSHOT_EXACT_MODE = true;
window.PPT_TARGET_WIDTH = ${targetWidth};
window.PPT_TARGET_HEIGHT = ${targetHeight};
// 立即执行尺寸强制设置
(function() {
const exactWidth = ${targetWidth};
const exactHeight = ${targetHeight};
console.log('截图模式强制设置:', exactWidth + 'x' + exactHeight);
// 强制设置根元素
const html = document.documentElement;
const body = document.body;
html.style.cssText = \`
width: \${exactWidth}px !important;
height: \${exactHeight}px !important;
min-width: \${exactWidth}px !important;
min-height: \${exactHeight}px !important;
max-width: \${exactWidth}px !important;
max-height: \${exactHeight}px !important;
overflow: hidden !important;
margin: 0 !important;
padding: 0 !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
border: none !important;
outline: none !important;
transform: none !important;
transform-origin: top left !important;
\`;
body.style.cssText = \`
width: \${exactWidth}px !important;
height: \${exactHeight}px !important;
min-width: \${exactWidth}px !important;
min-height: \${exactHeight}px !important;
max-width: \${exactWidth}px !important;
max-height: \${exactHeight}px !important;
overflow: hidden !important;
margin: 0 !important;
padding: 0 !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
border: none !important;
outline: none !important;
transform: none !important;
transform-origin: top left !important;
\`;
// 设置容器
const container = document.querySelector('.slide-container');
if (container) {
container.style.cssText = \`
width: \${exactWidth}px !important;
height: \${exactHeight}px !important;
min-width: \${exactWidth}px !important;
min-height: \${exactHeight}px !important;
max-width: \${exactWidth}px !important;
max-height: \${exactHeight}px !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
overflow: hidden !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
outline: none !important;
transform: none !important;
box-shadow: none !important;
z-index: 1 !important;
\`;
}
// 移除响应式类
html.classList.remove('browse-mode');
body.classList.remove('browse-mode');
console.log('截图模式设置完成');
})();
// DOM加载完成后再次确认
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM加载完成,最终确认尺寸设置');
// 再次强制设置,确保完全生效
const html = document.documentElement;
const body = document.body;
const container = document.querySelector('.slide-container');
[html, body, container].forEach(element => {
if (element) {
element.style.width = '${targetWidth}px';
element.style.height = '${targetHeight}px';
element.style.minWidth = '${targetWidth}px';
element.style.minHeight = '${targetHeight}px';
element.style.maxWidth = '${targetWidth}px';
element.style.maxHeight = '${targetHeight}px';
element.style.margin = '0';
element.style.padding = '0';
element.style.border = 'none';
element.style.outline = 'none';
element.style.overflow = 'hidden';
if (element !== container) {
element.style.position = 'fixed';
element.style.top = '0';
element.style.left = '0';
}
}
});
// 最终验证
setTimeout(() => {
console.log('最终尺寸验证:', {
html: html.offsetWidth + 'x' + html.offsetHeight,
body: body.offsetWidth + 'x' + body.offsetHeight,
container: container ? container.offsetWidth + 'x' + container.offsetHeight : 'none',
target: '${targetWidth}x${targetHeight}'
});
}, 100);
});
</script>
</body>`
);
console.log('HTML修改完成,已注入截图优化代码');
return finalHtml;
}
async captureScreenshot(htmlContent, width, height, options = {}) {
try {
await this.initBrowser();
console.log(`开始截图, 目标尺寸: ${width}x${height}`);
// 修改HTML内容以适应截图
const optimizedHtml = await this.modifyHtmlForScreenshot(htmlContent, width, height);
// 创建新页面
const page = await this.browser.newPage();
try {
// 设置精确的viewport尺寸
await page.setViewport({
width: width,
height: 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');
// 最终强制设置
[html, body].forEach(element => {
if (element) {
element.style.width = targetWidth + 'px';
element.style.height = targetHeight + 'px';
element.style.minWidth = targetWidth + 'px';
element.style.minHeight = targetHeight + 'px';
element.style.maxWidth = targetWidth + 'px';
element.style.maxHeight = targetHeight + 'px';
element.style.margin = '0';
element.style.padding = '0';
element.style.border = 'none';
element.style.outline = 'none';
element.style.overflow = 'hidden';
element.style.position = 'fixed';
element.style.top = '0';
element.style.left = '0';
}
});
if (container) {
container.style.width = targetWidth + 'px';
container.style.height = targetHeight + 'px';
container.style.minWidth = targetWidth + 'px';
container.style.minHeight = targetHeight + 'px';
container.style.maxWidth = targetWidth + 'px';
container.style.maxHeight = targetHeight + 'px';
container.style.position = 'fixed';
container.style.top = '0';
container.style.left = '0';
container.style.margin = '0';
container.style.padding = '0';
container.style.border = 'none';
container.style.outline = 'none';
container.style.overflow = 'hidden';
container.style.transform = 'none';
container.style.boxShadow = 'none';
}
console.log('最终页面尺寸确认:', {
html: html.offsetWidth + 'x' + html.offsetHeight,
body: body.offsetWidth + 'x' + body.offsetHeight,
container: container ? container.offsetWidth + 'x' + container.offsetHeight : 'none'
});
}, width, height);
// 再次等待以确保所有更改生效
await page.waitForTimeout(1000);
// 执行截图,使用精确的剪裁区域
const screenshot = await page.screenshot({
type: options.format || 'png',
quality: options.quality || 100,
clip: {
x: 0,
y: 0,
width: width,
height: height
},
omitBackground: false, // 包含背景
captureBeyondViewport: false, // 不截取视口外内容
});
console.log(`截图完成, 生成了 ${screenshot.length} 字节的图片数据`);
return screenshot;
} finally {
await page.close();
}
} catch (error) {
console.error('截图失败:', error);
throw new Error(`截图失败: ${error.message}`);
}
}
}
export default new ScreenshotService();