|
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浏览器已关闭');
|
|
}
|
|
}
|
|
|
|
|
|
extractPPTDimensions(htmlContent) {
|
|
try {
|
|
|
|
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])
|
|
};
|
|
}
|
|
|
|
|
|
const viewportMatch = htmlContent.match(/width=(\d+),\s*height=(\d+)/);
|
|
if (viewportMatch) {
|
|
return {
|
|
width: parseInt(viewportMatch[1]),
|
|
height: parseInt(viewportMatch[2])
|
|
};
|
|
}
|
|
|
|
|
|
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 };
|
|
}
|
|
}
|
|
|
|
|
|
optimizeHtmlForScreenshot(htmlContent, targetWidth, targetHeight) {
|
|
console.log(`优化HTML for精确截图, 目标尺寸: ${targetWidth}x${targetHeight}`);
|
|
|
|
|
|
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>`
|
|
);
|
|
|
|
|
|
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('开始生成截图...');
|
|
|
|
|
|
const dimensions = this.extractPPTDimensions(htmlContent);
|
|
console.log(`检测到PPT尺寸: ${dimensions.width}x${dimensions.height}`);
|
|
|
|
|
|
const optimizedHtml = this.optimizeHtmlForScreenshot(htmlContent, dimensions.width, dimensions.height);
|
|
|
|
|
|
const page = await this.browser.newPage();
|
|
|
|
try {
|
|
|
|
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(); |