|
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}`);
|
|
|
|
|
|
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>`
|
|
);
|
|
|
|
|
|
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}`);
|
|
|
|
|
|
const optimizedHtml = await this.modifyHtmlForScreenshot(htmlContent, width, height);
|
|
|
|
|
|
const page = await this.browser.newPage();
|
|
|
|
try {
|
|
|
|
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(); |