|
import puppeteer from 'puppeteer';
|
|
|
|
class ScreenshotService {
|
|
async generateScreenshot(htmlContent, options = {}) {
|
|
let browser = null;
|
|
let pptDimensions = { width: 1000, height: 750 };
|
|
|
|
try {
|
|
console.log('Starting Puppeteer browser...');
|
|
|
|
|
|
pptDimensions = this.extractPptDimensions(htmlContent);
|
|
console.log('Extracted PPT dimensions:', pptDimensions);
|
|
|
|
|
|
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',
|
|
'--disable-gpu',
|
|
'--disable-background-timer-throttling',
|
|
'--disable-backgrounding-occluded-windows',
|
|
'--disable-renderer-backgrounding',
|
|
'--disable-web-security',
|
|
'--disable-features=TranslateUI',
|
|
'--disable-extensions',
|
|
'--disable-component-extensions-with-background-pages',
|
|
'--disable-default-apps',
|
|
'--mute-audio',
|
|
'--no-default-browser-check',
|
|
'--autoplay-policy=user-gesture-required',
|
|
'--disable-background-mode',
|
|
'--disable-plugins',
|
|
'--disable-translate',
|
|
'--disable-ipc-flooding-protection',
|
|
'--memory-pressure-off',
|
|
'--max_old_space_size=4096'
|
|
],
|
|
timeout: 30000,
|
|
protocolTimeout: 30000
|
|
});
|
|
|
|
console.log('Browser launched successfully');
|
|
|
|
const page = await browser.newPage();
|
|
console.log('New page created');
|
|
|
|
|
|
await page.setViewport({
|
|
width: pptDimensions.width,
|
|
height: pptDimensions.height,
|
|
deviceScaleFactor: 2
|
|
});
|
|
|
|
console.log(`Viewport set to exact PPT size: ${pptDimensions.width}x${pptDimensions.height}`);
|
|
|
|
|
|
const modifiedHtmlContent = this.modifyHtmlForScreenshot(htmlContent, pptDimensions);
|
|
|
|
|
|
await page.setContent(modifiedHtmlContent, {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 10000
|
|
});
|
|
|
|
console.log('HTML content set');
|
|
|
|
|
|
await page.waitForTimeout(1500);
|
|
|
|
|
|
const actualDimensions = await page.evaluate(() => {
|
|
const container = document.getElementById('slideContainer');
|
|
const body = document.body;
|
|
const html = document.documentElement;
|
|
|
|
if (container) {
|
|
const rect = container.getBoundingClientRect();
|
|
return {
|
|
container: {
|
|
width: rect.width,
|
|
height: rect.height,
|
|
left: rect.left,
|
|
top: rect.top
|
|
},
|
|
body: {
|
|
width: body.offsetWidth,
|
|
height: body.offsetHeight
|
|
},
|
|
html: {
|
|
width: html.offsetWidth,
|
|
height: html.offsetHeight
|
|
},
|
|
viewport: {
|
|
width: window.innerWidth,
|
|
height: window.innerHeight
|
|
}
|
|
};
|
|
}
|
|
return null;
|
|
});
|
|
|
|
console.log('Page dimensions verification:', actualDimensions);
|
|
|
|
|
|
if (actualDimensions && (
|
|
Math.abs(actualDimensions.viewport.width - pptDimensions.width) > 1 ||
|
|
Math.abs(actualDimensions.viewport.height - pptDimensions.height) > 1
|
|
)) {
|
|
console.warn('Page dimensions do not match PPT dimensions exactly, adjusting...');
|
|
await page.setViewport({
|
|
width: pptDimensions.width,
|
|
height: pptDimensions.height,
|
|
deviceScaleFactor: 2
|
|
});
|
|
await page.waitForTimeout(500);
|
|
}
|
|
|
|
console.log('Taking precise screenshot...');
|
|
|
|
|
|
const screenshot = await page.screenshot({
|
|
type: 'jpeg',
|
|
quality: 95,
|
|
|
|
clip: {
|
|
x: 0,
|
|
y: 0,
|
|
width: pptDimensions.width,
|
|
height: pptDimensions.height
|
|
},
|
|
timeout: 10000
|
|
});
|
|
|
|
console.log('Screenshot taken successfully, size:', screenshot.length);
|
|
|
|
return screenshot;
|
|
} catch (error) {
|
|
console.error('Screenshot generation error:', error);
|
|
|
|
|
|
return this.generateErrorImage(error.message, pptDimensions);
|
|
} finally {
|
|
if (browser) {
|
|
try {
|
|
await browser.close();
|
|
console.log('Browser closed');
|
|
} catch (closeError) {
|
|
console.error('Error closing browser:', closeError);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
extractPptDimensions(htmlContent) {
|
|
|
|
const dimensionsMatch = htmlContent.match(/window\.PPT_DIMENSIONS\s*=\s*{\s*width:\s*(\d+),\s*height:\s*(\d+)\s*}/);
|
|
if (dimensionsMatch) {
|
|
const width = parseInt(dimensionsMatch[1]);
|
|
const height = parseInt(dimensionsMatch[2]);
|
|
console.log('Extracted dimensions from JavaScript:', { width, height });
|
|
return { width, height };
|
|
}
|
|
|
|
|
|
const cssMatch = htmlContent.match(/\.slide-container\s*{\s*[^}]*width:\s*(\d+)px;[^}]*height:\s*(\d+)px;/s);
|
|
if (cssMatch) {
|
|
const width = parseInt(cssMatch[1]);
|
|
const height = parseInt(cssMatch[2]);
|
|
console.log('Extracted dimensions from CSS:', { width, height });
|
|
return { width, height };
|
|
}
|
|
|
|
|
|
const inlineMatch = htmlContent.match(/width:\s*(\d+)px[^;]*;\s*height:\s*(\d+)px/);
|
|
if (inlineMatch) {
|
|
const width = parseInt(inlineMatch[1]);
|
|
const height = parseInt(inlineMatch[2]);
|
|
console.log('Extracted dimensions from inline styles:', { width, height });
|
|
return { width, height };
|
|
}
|
|
|
|
|
|
console.warn('Could not extract PPT dimensions from HTML, using default 16:9 ratio');
|
|
return { width: 1280, height: 720 };
|
|
}
|
|
|
|
|
|
modifyHtmlForScreenshot(htmlContent, pptDimensions) {
|
|
const { width, height } = pptDimensions;
|
|
|
|
|
|
let modifiedHtml = htmlContent;
|
|
|
|
|
|
modifiedHtml = modifiedHtml.replace(
|
|
/<style>[\s\S]*?<\/style>/,
|
|
`<style>
|
|
* {
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
box-sizing: border-box !important;
|
|
border: none !important;
|
|
}
|
|
|
|
html {
|
|
width: ${width}px !important;
|
|
height: ${height}px !important;
|
|
overflow: hidden !important;
|
|
background: transparent !important;
|
|
min-width: ${width}px !important;
|
|
min-height: ${height}px !important;
|
|
max-width: ${width}px !important;
|
|
max-height: ${height}px !important;
|
|
}
|
|
|
|
body {
|
|
width: ${width}px !important;
|
|
height: ${height}px !important;
|
|
overflow: hidden !important;
|
|
font-family: 'Microsoft YaHei', Arial, sans-serif !important;
|
|
background: transparent !important;
|
|
position: absolute !important;
|
|
top: 0 !important;
|
|
left: 0 !important;
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
min-width: ${width}px !important;
|
|
min-height: ${height}px !important;
|
|
max-width: ${width}px !important;
|
|
max-height: ${height}px !important;
|
|
}
|
|
|
|
.slide-container {
|
|
width: ${width}px !important;
|
|
height: ${height}px !important;
|
|
position: absolute !important;
|
|
top: 0 !important;
|
|
left: 0 !important;
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
overflow: hidden !important;
|
|
background-color: var(--slide-bg-color, #ffffff) !important;
|
|
transform: none !important;
|
|
transform-origin: top left !important;
|
|
min-width: ${width}px !important;
|
|
min-height: ${height}px !important;
|
|
max-width: ${width}px !important;
|
|
max-height: ${height}px !important;
|
|
}
|
|
|
|
/* 确保所有元素都相对于slide-container定位 */
|
|
.slide-container > * {
|
|
position: absolute !important;
|
|
}
|
|
|
|
/* 确保背景图片也严格匹配尺寸 */
|
|
.slide-container::before {
|
|
width: ${width}px !important;
|
|
height: ${height}px !important;
|
|
top: 0 !important;
|
|
left: 0 !important;
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
}
|
|
</style>`
|
|
);
|
|
|
|
|
|
modifiedHtml = modifiedHtml.replace(
|
|
/<script>[\s\S]*?<\/script>/g,
|
|
`<script>
|
|
// 截图模式 - 固定尺寸,消除白边
|
|
console.log('Screenshot mode - exact dimensions: ${width}x${height}');
|
|
|
|
// 确保页面加载完成后设置精确的尺寸
|
|
window.addEventListener('load', function() {
|
|
// 设置容器精确尺寸
|
|
const container = document.getElementById('slideContainer');
|
|
if (container) {
|
|
container.style.cssText = \`
|
|
width: ${width}px !important;
|
|
height: ${height}px !important;
|
|
position: absolute !important;
|
|
top: 0 !important;
|
|
left: 0 !important;
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
transform: none !important;
|
|
overflow: hidden !important;
|
|
\`;
|
|
}
|
|
|
|
// 设置body和html精确尺寸
|
|
document.body.style.cssText = \`
|
|
width: ${width}px !important;
|
|
height: ${height}px !important;
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
overflow: hidden !important;
|
|
position: absolute !important;
|
|
top: 0 !important;
|
|
left: 0 !important;
|
|
\`;
|
|
|
|
document.documentElement.style.cssText = \`
|
|
width: ${width}px !important;
|
|
height: ${height}px !important;
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
overflow: hidden !important;
|
|
\`;
|
|
|
|
console.log('Exact dimensions applied for screenshot');
|
|
});
|
|
|
|
// 禁用所有可能的缩放和调整
|
|
window.addEventListener('resize', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
});
|
|
</script>`
|
|
);
|
|
|
|
return modifiedHtml;
|
|
}
|
|
|
|
|
|
generateErrorImage(errorMessage = '截图生成失败', dimensions = { width: 1280, height: 720 }) {
|
|
const { width, height } = dimensions;
|
|
|
|
const canvas = `
|
|
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
<rect width="${width}" height="${height}" fill="#f8f9fa"/>
|
|
<rect x="50" y="50" width="${width - 100}" height="${height - 100}" fill="white" stroke="#dee2e6" stroke-width="2"/>
|
|
<text x="${width / 2}" y="${height / 2 - 20}" text-anchor="middle" font-family="Arial" font-size="24" fill="#6c757d">
|
|
截图生成失败
|
|
</text>
|
|
<text x="${width / 2}" y="${height / 2 + 20}" text-anchor="middle" font-family="Arial" font-size="16" fill="#868e96">
|
|
${errorMessage}
|
|
</text>
|
|
</svg>
|
|
`;
|
|
|
|
return Buffer.from(canvas);
|
|
}
|
|
}
|
|
|
|
export default new ScreenshotService(); |