web_ppt / backend /src /services /screenshotService.js
CatPtain's picture
Upload screenshotService.js
1d4c41b verified
raw
history blame
12.7 kB
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...');
// 提取PPT的实际尺寸和长宽比
pptDimensions = this.extractPptDimensions(htmlContent);
console.log('Extracted PPT dimensions:', pptDimensions);
// 更适合容器环境的Puppeteer配置
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');
// 设置页面视窗大小为PPT的精确尺寸
await page.setViewport({
width: pptDimensions.width,
height: pptDimensions.height,
deviceScaleFactor: 2 // 高分辨率
});
console.log(`Viewport set to exact PPT size: ${pptDimensions.width}x${pptDimensions.height}`);
// 修改HTML内容,确保PPT容器精确匹配视窗尺寸,消除白边
const modifiedHtmlContent = this.modifyHtmlForScreenshot(htmlContent, pptDimensions);
// 设置HTML内容
await page.setContent(modifiedHtmlContent, {
waitUntil: 'domcontentloaded',
timeout: 10000
});
console.log('HTML content set');
// 等待渲染完成
await page.waitForTimeout(1500);
// 验证页面中的PPT容器尺寸是否精确匹配
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);
// 确保页面尺寸完全匹配PPT尺寸
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...');
// 截取精确的PPT内容区域,不包含任何白边
const screenshot = await page.screenshot({
type: 'jpeg',
quality: 95,
// 使用clip精确截取PPT内容区域
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);
}
}
}
}
// 从HTML内容中提取PPT尺寸信息和长宽比
extractPptDimensions(htmlContent) {
// 方法1:从JavaScript变量中提取尺寸 - 这是最准确的方法
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 };
}
// 方法2:从CSS样式中提取尺寸
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 };
}
// 方法3:从内联样式中提取尺寸
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 };
}
// 默认尺寸 - 使用标准PPT 16:9 比例
console.warn('Could not extract PPT dimensions from HTML, using default 16:9 ratio');
return { width: 1280, height: 720 }; // 16:9 标准比例
}
// 修改HTML内容以适应截图需求,确保零白边
modifyHtmlForScreenshot(htmlContent, pptDimensions) {
const { width, height } = pptDimensions;
// 创建完全匹配PPT尺寸的HTML,消除所有可能的白边
let modifiedHtml = htmlContent;
// 设置严格的CSS样式,确保页面尺寸完全匹配PPT尺寸
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>`
);
// 移除所有可能影响尺寸的JavaScript,确保固定尺寸
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();