web_ppt / backend /src /services /screenshotService.js
CatPtain's picture
Upload screenshotService.js
d1278b8 verified
raw
history blame
18.2 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',
'--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}`);
// 在<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;
-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,
.browse-mode html,
.browse-mode body,
.browse-mode .slide-container {
display: block !important;
position: fixed !important;
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;
transform: none !important;
background-color: transparent !important;
top: 0 !important;
left: 0 !important;
margin: 0 !important;
padding: 0 !important;
box-shadow: none !important;
}
</style>`
);
// 在</body>前插入截图专用JavaScript
const finalHtml = optimizedHtml.replace(
/(<\/body>)/i,
`
<script id="screenshot-precision-script">
// 截图模式强制设置
window.SCREENSHOT_EXACT_MODE = true;
window.PPT_TARGET_WIDTH = ${targetWidth};
window.PPT_TARGET_HEIGHT = ${targetHeight};
// 立即执行强制设置,确保尺寸精确
(function() {
const targetW = ${targetWidth};
const targetH = ${targetHeight};
console.log('截图模式精确设置开始:', targetW + 'x' + targetH);
const html = document.documentElement;
const body = document.body;
// 强制设置根元素
const setElementSize = (element, width, height) => {
if (!element) return;
element.style.cssText = \`
width: \${width}px !important;
height: \${height}px !important;
min-width: \${width}px !important;
min-height: \${height}px !important;
max-width: \${width}px !important;
max-height: \${height}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;
box-sizing: border-box !important;
\`;
};
// 设置HTML和Body
setElementSize(html, targetW, targetH);
setElementSize(body, targetW, targetH);
// 移除可能的响应式类
html.classList.remove('browse-mode');
body.classList.remove('browse-mode');
// 设置容器
const container = document.querySelector('.slide-container');
if (container) {
container.style.cssText = \`
width: \${targetW}px !important;
height: \${targetH}px !important;
min-width: \${targetW}px !important;
min-height: \${targetH}px !important;
max-width: \${targetW}px !important;
max-height: \${targetH}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;
\`;
}
console.log('截图模式精确设置完成');
})();
// DOM加载完成后再次确认
document.addEventListener('DOMContentLoaded', function() {
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';
element.style.transform = 'none';
}
});
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('DOM加载后的最终尺寸确认完成');
});
</script>
$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();