Upload screenshotService.js
Browse files
backend/src/services/screenshotService.js
CHANGED
@@ -4,13 +4,15 @@ class ScreenshotService {
|
|
4 |
constructor() {
|
5 |
this.browser = null;
|
6 |
this.isInitialized = false;
|
7 |
-
this.maxRetries =
|
8 |
this.browserLaunchRetries = 0;
|
9 |
-
this.maxBrowserLaunchRetries =
|
10 |
this.isClosing = false;
|
|
|
11 |
}
|
12 |
|
13 |
async initBrowser() {
|
|
|
14 |
if (this.isClosing) {
|
15 |
console.log('浏览器正在关闭中,等待重新初始化...');
|
16 |
this.browser = null;
|
@@ -20,16 +22,17 @@ class ScreenshotService {
|
|
20 |
if (!this.browser) {
|
21 |
try {
|
22 |
console.log('初始化Puppeteer浏览器...');
|
23 |
-
|
|
|
24 |
headless: 'new',
|
|
|
|
|
25 |
args: [
|
26 |
'--no-sandbox',
|
27 |
'--disable-setuid-sandbox',
|
28 |
'--disable-dev-shm-usage',
|
29 |
'--disable-accelerated-2d-canvas',
|
30 |
'--no-first-run',
|
31 |
-
'--no-zygote',
|
32 |
-
'--single-process',
|
33 |
'--disable-gpu',
|
34 |
'--disable-background-timer-throttling',
|
35 |
'--disable-backgrounding-occluded-windows',
|
@@ -44,20 +47,31 @@ class ScreenshotService {
|
|
44 |
'--disable-sync',
|
45 |
'--metrics-recording-only',
|
46 |
'--disable-domain-reliability',
|
47 |
-
'--disable-component-extensions-with-background-pages',
|
48 |
'--force-device-scale-factor=1',
|
49 |
-
'--enable-precise-memory-info',
|
50 |
'--disable-features=VizDisplayCompositor',
|
51 |
'--run-all-compositor-stages-before-draw',
|
52 |
'--disable-new-content-rendering-timeout',
|
53 |
'--disable-ipc-flooding-protection',
|
54 |
'--disable-hang-monitor',
|
55 |
'--disable-prompt-on-repost',
|
56 |
-
'--
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
61 |
|
62 |
// 监听浏览器断开连接事件
|
63 |
this.browser.on('disconnected', () => {
|
@@ -66,16 +80,6 @@ class ScreenshotService {
|
|
66 |
this.isClosing = false;
|
67 |
});
|
68 |
|
69 |
-
// 监听目标创建事件
|
70 |
-
this.browser.on('targetcreated', (target) => {
|
71 |
-
console.log('新目标创建:', target.type());
|
72 |
-
});
|
73 |
-
|
74 |
-
// 监听目标销毁事件
|
75 |
-
this.browser.on('targetdestroyed', (target) => {
|
76 |
-
console.log('目标销毁:', target.type());
|
77 |
-
});
|
78 |
-
|
79 |
console.log('Puppeteer浏览器初始化完成');
|
80 |
this.browserLaunchRetries = 0;
|
81 |
} catch (error) {
|
@@ -84,21 +88,25 @@ class ScreenshotService {
|
|
84 |
|
85 |
if (this.browserLaunchRetries <= this.maxBrowserLaunchRetries) {
|
86 |
console.log(`尝试重新初始化浏览器 (${this.browserLaunchRetries}/${this.maxBrowserLaunchRetries})`);
|
87 |
-
await this.delay(
|
88 |
return this.initBrowser();
|
89 |
} else {
|
90 |
-
|
|
|
|
|
91 |
}
|
92 |
}
|
93 |
}
|
94 |
|
95 |
// 验证浏览器是否仍然连接
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
|
|
|
|
102 |
}
|
103 |
|
104 |
return this.browser;
|
@@ -146,15 +154,6 @@ class ScreenshotService {
|
|
146 |
};
|
147 |
}
|
148 |
|
149 |
-
// 从CSS中提取
|
150 |
-
const cssMatch = htmlContent.match(/width:\s*(\d+)px.*height:\s*(\d+)px/);
|
151 |
-
if (cssMatch) {
|
152 |
-
return {
|
153 |
-
width: parseInt(cssMatch[1]),
|
154 |
-
height: parseInt(cssMatch[2])
|
155 |
-
};
|
156 |
-
}
|
157 |
-
|
158 |
// 默认尺寸
|
159 |
return { width: 960, height: 720 };
|
160 |
} catch (error) {
|
@@ -163,6 +162,31 @@ class ScreenshotService {
|
|
163 |
}
|
164 |
}
|
165 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
166 |
// 优化HTML内容以确保精确截图
|
167 |
optimizeHtmlForScreenshot(htmlContent, targetWidth, targetHeight) {
|
168 |
console.log(`优化HTML for精确截图, 目标尺寸: ${targetWidth}x${targetHeight}`);
|
@@ -244,9 +268,7 @@ class ScreenshotService {
|
|
244 |
height: 0 !important;
|
245 |
}
|
246 |
|
247 |
-
html {
|
248 |
-
scrollbar-width: none !important;
|
249 |
-
}
|
250 |
|
251 |
/* 禁用用户交互 */
|
252 |
* {
|
@@ -254,179 +276,22 @@ class ScreenshotService {
|
|
254 |
-moz-user-select: none !important;
|
255 |
-ms-user-select: none !important;
|
256 |
user-select: none !important;
|
257 |
-
-webkit-user-drag: none !important;
|
258 |
-
-khtml-user-drag: none !important;
|
259 |
-
-moz-user-drag: none !important;
|
260 |
-
-o-user-drag: none !important;
|
261 |
-
user-drag: none !important;
|
262 |
pointer-events: none !important;
|
263 |
}
|
264 |
-
|
265 |
-
/* 强制移除响应式样式 */
|
266 |
-
.browse-mode,
|
267 |
-
.browse-mode html,
|
268 |
-
.browse-mode body,
|
269 |
-
.browse-mode .slide-container {
|
270 |
-
display: block !important;
|
271 |
-
position: fixed !important;
|
272 |
-
width: ${targetWidth}px !important;
|
273 |
-
height: ${targetHeight}px !important;
|
274 |
-
min-width: ${targetWidth}px !important;
|
275 |
-
min-height: ${targetHeight}px !important;
|
276 |
-
max-width: ${targetWidth}px !important;
|
277 |
-
max-height: ${targetHeight}px !important;
|
278 |
-
transform: none !important;
|
279 |
-
background-color: transparent !important;
|
280 |
-
top: 0 !important;
|
281 |
-
left: 0 !important;
|
282 |
-
margin: 0 !important;
|
283 |
-
padding: 0 !important;
|
284 |
-
box-shadow: none !important;
|
285 |
-
}
|
286 |
</style>`
|
287 |
);
|
288 |
|
289 |
-
|
290 |
-
const finalHtml = optimizedHtml.replace(
|
291 |
-
/(<\/body>)/i,
|
292 |
-
`
|
293 |
-
<script id="screenshot-precision-script">
|
294 |
-
// 截图模式强制设置
|
295 |
-
window.SCREENSHOT_EXACT_MODE = true;
|
296 |
-
window.PPT_TARGET_WIDTH = ${targetWidth};
|
297 |
-
window.PPT_TARGET_HEIGHT = ${targetHeight};
|
298 |
-
|
299 |
-
// 立即执行强制设置,确保尺寸精确
|
300 |
-
(function() {
|
301 |
-
const targetW = ${targetWidth};
|
302 |
-
const targetH = ${targetHeight};
|
303 |
-
|
304 |
-
console.log('截图模式精确设置开始:', targetW + 'x' + targetH);
|
305 |
-
|
306 |
-
const html = document.documentElement;
|
307 |
-
const body = document.body;
|
308 |
-
|
309 |
-
// 强制设置根元素
|
310 |
-
const setElementSize = (element, width, height) => {
|
311 |
-
if (!element) return;
|
312 |
-
|
313 |
-
element.style.cssText = \`
|
314 |
-
width: \${width}px !important;
|
315 |
-
height: \${height}px !important;
|
316 |
-
min-width: \${width}px !important;
|
317 |
-
min-height: \${height}px !important;
|
318 |
-
max-width: \${width}px !important;
|
319 |
-
max-height: \${height}px !important;
|
320 |
-
overflow: hidden !important;
|
321 |
-
margin: 0 !important;
|
322 |
-
padding: 0 !important;
|
323 |
-
position: fixed !important;
|
324 |
-
top: 0 !important;
|
325 |
-
left: 0 !important;
|
326 |
-
border: none !important;
|
327 |
-
outline: none !important;
|
328 |
-
transform: none !important;
|
329 |
-
transform-origin: top left !important;
|
330 |
-
box-sizing: border-box !important;
|
331 |
-
\`;
|
332 |
-
};
|
333 |
-
|
334 |
-
// 设置HTML和Body
|
335 |
-
setElementSize(html, targetW, targetH);
|
336 |
-
setElementSize(body, targetW, targetH);
|
337 |
-
|
338 |
-
// 移除可能的响应式类
|
339 |
-
html.classList.remove('browse-mode');
|
340 |
-
body.classList.remove('browse-mode');
|
341 |
-
|
342 |
-
// 设置容器
|
343 |
-
const container = document.querySelector('.slide-container');
|
344 |
-
if (container) {
|
345 |
-
container.style.cssText = \`
|
346 |
-
width: \${targetW}px !important;
|
347 |
-
height: \${targetH}px !important;
|
348 |
-
min-width: \${targetW}px !important;
|
349 |
-
min-height: \${targetH}px !important;
|
350 |
-
max-width: \${targetW}px !important;
|
351 |
-
max-height: \${targetH}px !important;
|
352 |
-
position: fixed !important;
|
353 |
-
top: 0 !important;
|
354 |
-
left: 0 !important;
|
355 |
-
overflow: hidden !important;
|
356 |
-
margin: 0 !important;
|
357 |
-
padding: 0 !important;
|
358 |
-
border: none !important;
|
359 |
-
outline: none !important;
|
360 |
-
transform: none !important;
|
361 |
-
box-shadow: none !important;
|
362 |
-
z-index: 1 !important;
|
363 |
-
\`;
|
364 |
-
}
|
365 |
-
|
366 |
-
console.log('截图模式精确设置完成');
|
367 |
-
})();
|
368 |
-
|
369 |
-
// DOM加载完成后再次确认
|
370 |
-
document.addEventListener('DOMContentLoaded', function() {
|
371 |
-
const html = document.documentElement;
|
372 |
-
const body = document.body;
|
373 |
-
const container = document.querySelector('.slide-container');
|
374 |
-
|
375 |
-
// 最终强制设置,确保完全生效
|
376 |
-
[html, body].forEach(element => {
|
377 |
-
if (element) {
|
378 |
-
element.style.width = '${targetWidth}px';
|
379 |
-
element.style.height = '${targetHeight}px';
|
380 |
-
element.style.minWidth = '${targetWidth}px';
|
381 |
-
element.style.minHeight = '${targetHeight}px';
|
382 |
-
element.style.maxWidth = '${targetWidth}px';
|
383 |
-
element.style.maxHeight = '${targetHeight}px';
|
384 |
-
element.style.margin = '0';
|
385 |
-
element.style.padding = '0';
|
386 |
-
element.style.border = 'none';
|
387 |
-
element.style.outline = 'none';
|
388 |
-
element.style.overflow = 'hidden';
|
389 |
-
element.style.position = 'fixed';
|
390 |
-
element.style.top = '0';
|
391 |
-
element.style.left = '0';
|
392 |
-
element.style.transform = 'none';
|
393 |
-
}
|
394 |
-
});
|
395 |
-
|
396 |
-
if (container) {
|
397 |
-
container.style.width = '${targetWidth}px';
|
398 |
-
container.style.height = '${targetHeight}px';
|
399 |
-
container.style.minWidth = '${targetWidth}px';
|
400 |
-
container.style.minHeight = '${targetHeight}px';
|
401 |
-
container.style.maxWidth = '${targetWidth}px';
|
402 |
-
container.style.maxHeight = '${targetHeight}px';
|
403 |
-
container.style.position = 'fixed';
|
404 |
-
container.style.top = '0';
|
405 |
-
container.style.left = '0';
|
406 |
-
container.style.margin = '0';
|
407 |
-
container.style.padding = '0';
|
408 |
-
container.style.border = 'none';
|
409 |
-
container.style.outline = 'none';
|
410 |
-
container.style.overflow = 'hidden';
|
411 |
-
container.style.transform = 'none';
|
412 |
-
container.style.boxShadow = 'none';
|
413 |
-
}
|
414 |
-
|
415 |
-
console.log('DOM加载后的最终尺寸确认完成');
|
416 |
-
});
|
417 |
-
</script>
|
418 |
-
$1`
|
419 |
-
);
|
420 |
-
|
421 |
-
console.log('HTML优化完成,已注入精确尺寸控制代码');
|
422 |
-
return finalHtml;
|
423 |
}
|
424 |
|
425 |
-
async
|
426 |
try {
|
427 |
-
await this.initBrowser();
|
|
|
|
|
|
|
428 |
|
429 |
-
console.log(`开始生成截图... (尝试 ${retryCount + 1}/${this.maxRetries + 1})`);
|
430 |
|
431 |
// 从HTML中提取PPT的精确尺寸
|
432 |
const dimensions = this.extractPPTDimensions(htmlContent);
|
@@ -438,106 +303,43 @@ class ScreenshotService {
|
|
438 |
// 创建新页面
|
439 |
let page = null;
|
440 |
try {
|
441 |
-
page = await
|
442 |
|
443 |
// 设置页面超时
|
444 |
-
page.setDefaultTimeout(
|
445 |
-
page.setDefaultNavigationTimeout(
|
446 |
|
447 |
// 设置精确的viewport尺寸
|
448 |
await page.setViewport({
|
449 |
width: dimensions.width,
|
450 |
height: dimensions.height,
|
451 |
-
deviceScaleFactor: options.deviceScaleFactor ||
|
452 |
});
|
453 |
|
454 |
// 设置页面内容
|
455 |
await page.setContent(optimizedHtml, {
|
456 |
waitUntil: ['load', 'domcontentloaded'],
|
457 |
-
timeout:
|
458 |
});
|
459 |
|
460 |
// 等待页面完全渲染
|
461 |
-
await page.waitForTimeout(
|
462 |
-
|
463 |
-
// 执行最终的尺寸验证和强制设置
|
464 |
-
await page.evaluate((targetWidth, targetHeight) => {
|
465 |
-
const html = document.documentElement;
|
466 |
-
const body = document.body;
|
467 |
-
const container = document.querySelector('.slide-container');
|
468 |
-
|
469 |
-
// 最终强制设置,确保精确尺寸
|
470 |
-
const forceSize = (element, width, height) => {
|
471 |
-
if (!element) return;
|
472 |
-
|
473 |
-
element.style.width = width + 'px';
|
474 |
-
element.style.height = height + 'px';
|
475 |
-
element.style.minWidth = width + 'px';
|
476 |
-
element.style.minHeight = height + 'px';
|
477 |
-
element.style.maxWidth = width + 'px';
|
478 |
-
element.style.maxHeight = height + 'px';
|
479 |
-
element.style.margin = '0';
|
480 |
-
element.style.padding = '0';
|
481 |
-
element.style.border = 'none';
|
482 |
-
element.style.outline = 'none';
|
483 |
-
element.style.overflow = 'hidden';
|
484 |
-
element.style.transform = 'none';
|
485 |
-
|
486 |
-
if (element !== container) {
|
487 |
-
element.style.position = 'fixed';
|
488 |
-
element.style.top = '0';
|
489 |
-
element.style.left = '0';
|
490 |
-
}
|
491 |
-
};
|
492 |
-
|
493 |
-
forceSize(html, targetWidth, targetHeight);
|
494 |
-
forceSize(body, targetWidth, targetHeight);
|
495 |
-
|
496 |
-
if (container) {
|
497 |
-
forceSize(container, targetWidth, targetHeight);
|
498 |
-
container.style.position = 'fixed';
|
499 |
-
container.style.top = '0';
|
500 |
-
container.style.left = '0';
|
501 |
-
container.style.boxShadow = 'none';
|
502 |
-
container.style.zIndex = '1';
|
503 |
-
}
|
504 |
-
|
505 |
-
// 验证尺寸设置结果
|
506 |
-
const verification = {
|
507 |
-
html: html.offsetWidth + 'x' + html.offsetHeight,
|
508 |
-
body: body.offsetWidth + 'x' + body.offsetHeight,
|
509 |
-
container: container ? container.offsetWidth + 'x' + container.offsetHeight : 'none',
|
510 |
-
target: targetWidth + 'x' + targetHeight
|
511 |
-
};
|
512 |
-
|
513 |
-
console.log('最终尺寸验证:', verification);
|
514 |
-
|
515 |
-
// 如果尺寸不匹配,记录警告
|
516 |
-
if (html.offsetWidth !== targetWidth || html.offsetHeight !== targetHeight) {
|
517 |
-
console.warn('HTML尺寸不匹配目标尺寸!');
|
518 |
-
}
|
519 |
-
|
520 |
-
return verification;
|
521 |
-
}, dimensions.width, dimensions.height);
|
522 |
-
|
523 |
-
// 再次等待以确保所有更改生效
|
524 |
-
await page.waitForTimeout(1000);
|
525 |
|
526 |
// 执行截图,使用精确的剪裁区域
|
527 |
const screenshot = await page.screenshot({
|
528 |
type: options.format || 'jpeg',
|
529 |
-
quality: options.quality ||
|
530 |
clip: {
|
531 |
x: 0,
|
532 |
y: 0,
|
533 |
width: dimensions.width,
|
534 |
height: dimensions.height
|
535 |
},
|
536 |
-
omitBackground: false,
|
537 |
-
captureBeyondViewport: false,
|
538 |
});
|
539 |
|
540 |
-
console.log(
|
541 |
return screenshot;
|
542 |
|
543 |
} finally {
|
@@ -551,7 +353,7 @@ class ScreenshotService {
|
|
551 |
}
|
552 |
|
553 |
} catch (error) {
|
554 |
-
console.error(
|
555 |
|
556 |
// 如果是目标关闭错误或连接断开,重置浏览器
|
557 |
if (error.message.includes('Target closed') ||
|
@@ -564,17 +366,33 @@ class ScreenshotService {
|
|
564 |
|
565 |
// 如果还有重试机会,进行重试
|
566 |
if (retryCount < this.maxRetries) {
|
567 |
-
console.log(`等待 ${(retryCount + 1) *
|
568 |
-
await this.delay((retryCount + 1) *
|
569 |
-
return this.
|
570 |
}
|
571 |
|
572 |
-
throw
|
573 |
}
|
574 |
}
|
575 |
|
576 |
async generateScreenshot(htmlContent, options = {}) {
|
577 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
578 |
}
|
579 |
|
580 |
// 兼容旧方法名
|
|
|
4 |
constructor() {
|
5 |
this.browser = null;
|
6 |
this.isInitialized = false;
|
7 |
+
this.maxRetries = 2;
|
8 |
this.browserLaunchRetries = 0;
|
9 |
+
this.maxBrowserLaunchRetries = 2;
|
10 |
this.isClosing = false;
|
11 |
+
this.isHuggingFaceSpace = process.env.SPACE_ID || process.env.HF_SPACE_ID;
|
12 |
}
|
13 |
|
14 |
async initBrowser() {
|
15 |
+
// 在Hugging Face Space环境中,Puppeteer可能不稳定,尝试更保守的配置
|
16 |
if (this.isClosing) {
|
17 |
console.log('浏览器正在关闭中,等待重新初始化...');
|
18 |
this.browser = null;
|
|
|
22 |
if (!this.browser) {
|
23 |
try {
|
24 |
console.log('初始化Puppeteer浏览器...');
|
25 |
+
|
26 |
+
const launchOptions = {
|
27 |
headless: 'new',
|
28 |
+
timeout: 60000,
|
29 |
+
protocolTimeout: 60000,
|
30 |
args: [
|
31 |
'--no-sandbox',
|
32 |
'--disable-setuid-sandbox',
|
33 |
'--disable-dev-shm-usage',
|
34 |
'--disable-accelerated-2d-canvas',
|
35 |
'--no-first-run',
|
|
|
|
|
36 |
'--disable-gpu',
|
37 |
'--disable-background-timer-throttling',
|
38 |
'--disable-backgrounding-occluded-windows',
|
|
|
47 |
'--disable-sync',
|
48 |
'--metrics-recording-only',
|
49 |
'--disable-domain-reliability',
|
|
|
50 |
'--force-device-scale-factor=1',
|
|
|
51 |
'--disable-features=VizDisplayCompositor',
|
52 |
'--run-all-compositor-stages-before-draw',
|
53 |
'--disable-new-content-rendering-timeout',
|
54 |
'--disable-ipc-flooding-protection',
|
55 |
'--disable-hang-monitor',
|
56 |
'--disable-prompt-on-repost',
|
57 |
+
'--memory-pressure-off',
|
58 |
+
'--max_old_space_size=1024'
|
59 |
+
]
|
60 |
+
};
|
61 |
+
|
62 |
+
// 如果是Hugging Face Space环境,使用更保守的配置
|
63 |
+
if (this.isHuggingFaceSpace) {
|
64 |
+
console.log('检测到Hugging Face Space环境,使用保守配置');
|
65 |
+
launchOptions.args.push(
|
66 |
+
'--single-process',
|
67 |
+
'--no-zygote',
|
68 |
+
'--disable-web-security',
|
69 |
+
'--disable-features=site-per-process',
|
70 |
+
'--disable-site-isolation-trials'
|
71 |
+
);
|
72 |
+
}
|
73 |
+
|
74 |
+
this.browser = await puppeteer.launch(launchOptions);
|
75 |
|
76 |
// 监听浏览器断开连接事件
|
77 |
this.browser.on('disconnected', () => {
|
|
|
80 |
this.isClosing = false;
|
81 |
});
|
82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
83 |
console.log('Puppeteer浏览器初始化完成');
|
84 |
this.browserLaunchRetries = 0;
|
85 |
} catch (error) {
|
|
|
88 |
|
89 |
if (this.browserLaunchRetries <= this.maxBrowserLaunchRetries) {
|
90 |
console.log(`尝试重新初始化浏览器 (${this.browserLaunchRetries}/${this.maxBrowserLaunchRetries})`);
|
91 |
+
await this.delay(3000); // 等待3秒后重试
|
92 |
return this.initBrowser();
|
93 |
} else {
|
94 |
+
// 如果Puppeteer完全失败,返回null,使用fallback方法
|
95 |
+
console.warn('Puppeteer初始化完全失败,将使用fallback方法');
|
96 |
+
return null;
|
97 |
}
|
98 |
}
|
99 |
}
|
100 |
|
101 |
// 验证浏览器是否仍然连接
|
102 |
+
if (this.browser) {
|
103 |
+
try {
|
104 |
+
await this.browser.version();
|
105 |
+
} catch (error) {
|
106 |
+
console.warn('浏览器连接已断开,重新初始化:', error.message);
|
107 |
+
this.browser = null;
|
108 |
+
return this.initBrowser();
|
109 |
+
}
|
110 |
}
|
111 |
|
112 |
return this.browser;
|
|
|
154 |
};
|
155 |
}
|
156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
157 |
// 默认尺寸
|
158 |
return { width: 960, height: 720 };
|
159 |
} catch (error) {
|
|
|
162 |
}
|
163 |
}
|
164 |
|
165 |
+
// 生成简化的PNG图片作为fallback
|
166 |
+
generateFallbackImage(width, height, message = 'Screenshot not available') {
|
167 |
+
// 创建一个简单的SVG作为fallback
|
168 |
+
const svg = `
|
169 |
+
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
170 |
+
<rect width="100%" height="100%" fill="#f0f0f0"/>
|
171 |
+
<text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle"
|
172 |
+
font-family="Arial, sans-serif" font-size="24" fill="#666">
|
173 |
+
${message}
|
174 |
+
</text>
|
175 |
+
<text x="50%" y="60%" text-anchor="middle" dominant-baseline="middle"
|
176 |
+
font-family="Arial, sans-serif" font-size="16" fill="#999">
|
177 |
+
Size: ${width}x${height}
|
178 |
+
</text>
|
179 |
+
</svg>
|
180 |
+
`;
|
181 |
+
|
182 |
+
// 将SVG转换为base64编码的PNG
|
183 |
+
const base64Svg = Buffer.from(svg).toString('base64');
|
184 |
+
const dataUrl = `data:image/svg+xml;base64,${base64Svg}`;
|
185 |
+
|
186 |
+
// 返回简单的占位图片数据
|
187 |
+
return Buffer.from(svg, 'utf8');
|
188 |
+
}
|
189 |
+
|
190 |
// 优化HTML内容以确保精确截图
|
191 |
optimizeHtmlForScreenshot(htmlContent, targetWidth, targetHeight) {
|
192 |
console.log(`优化HTML for精确截图, 目标尺寸: ${targetWidth}x${targetHeight}`);
|
|
|
268 |
height: 0 !important;
|
269 |
}
|
270 |
|
271 |
+
html { scrollbar-width: none !important; }
|
|
|
|
|
272 |
|
273 |
/* 禁用用户交互 */
|
274 |
* {
|
|
|
276 |
-moz-user-select: none !important;
|
277 |
-ms-user-select: none !important;
|
278 |
user-select: none !important;
|
|
|
|
|
|
|
|
|
|
|
279 |
pointer-events: none !important;
|
280 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
281 |
</style>`
|
282 |
);
|
283 |
|
284 |
+
return optimizedHtml;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
285 |
}
|
286 |
|
287 |
+
async generateScreenshotWithPuppeteer(htmlContent, options = {}, retryCount = 0) {
|
288 |
try {
|
289 |
+
const browser = await this.initBrowser();
|
290 |
+
if (!browser) {
|
291 |
+
throw new Error('浏览器初始化失败');
|
292 |
+
}
|
293 |
|
294 |
+
console.log(`开始生成截图... (Puppeteer尝试 ${retryCount + 1}/${this.maxRetries + 1})`);
|
295 |
|
296 |
// 从HTML中提取PPT的精确尺寸
|
297 |
const dimensions = this.extractPPTDimensions(htmlContent);
|
|
|
303 |
// 创建新页面
|
304 |
let page = null;
|
305 |
try {
|
306 |
+
page = await browser.newPage();
|
307 |
|
308 |
// 设置页面超时
|
309 |
+
page.setDefaultTimeout(45000);
|
310 |
+
page.setDefaultNavigationTimeout(45000);
|
311 |
|
312 |
// 设置精确的viewport尺寸
|
313 |
await page.setViewport({
|
314 |
width: dimensions.width,
|
315 |
height: dimensions.height,
|
316 |
+
deviceScaleFactor: options.deviceScaleFactor || 1, // 降低设备缩放因子以减少内存使用
|
317 |
});
|
318 |
|
319 |
// 设置页面内容
|
320 |
await page.setContent(optimizedHtml, {
|
321 |
waitUntil: ['load', 'domcontentloaded'],
|
322 |
+
timeout: 45000
|
323 |
});
|
324 |
|
325 |
// 等待页面完全渲染
|
326 |
+
await page.waitForTimeout(1500);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
327 |
|
328 |
// 执行截图,使用精确的剪裁区域
|
329 |
const screenshot = await page.screenshot({
|
330 |
type: options.format || 'jpeg',
|
331 |
+
quality: options.quality || 90,
|
332 |
clip: {
|
333 |
x: 0,
|
334 |
y: 0,
|
335 |
width: dimensions.width,
|
336 |
height: dimensions.height
|
337 |
},
|
338 |
+
omitBackground: false,
|
339 |
+
captureBeyondViewport: false,
|
340 |
});
|
341 |
|
342 |
+
console.log(`Puppeteer截图成功生成,尺寸: ${dimensions.width}x${dimensions.height}, 数据大小: ${screenshot.length} 字节`);
|
343 |
return screenshot;
|
344 |
|
345 |
} finally {
|
|
|
353 |
}
|
354 |
|
355 |
} catch (error) {
|
356 |
+
console.error(`Puppeteer截图生成失败 (尝试 ${retryCount + 1}):`, error.message);
|
357 |
|
358 |
// 如果是目标关闭错误或连接断开,重置浏览器
|
359 |
if (error.message.includes('Target closed') ||
|
|
|
366 |
|
367 |
// 如果还有重试机会,进行重试
|
368 |
if (retryCount < this.maxRetries) {
|
369 |
+
console.log(`等待 ${(retryCount + 1) * 3} 秒后重试...`);
|
370 |
+
await this.delay((retryCount + 1) * 3000);
|
371 |
+
return this.generateScreenshotWithPuppeteer(htmlContent, options, retryCount + 1);
|
372 |
}
|
373 |
|
374 |
+
throw error;
|
375 |
}
|
376 |
}
|
377 |
|
378 |
async generateScreenshot(htmlContent, options = {}) {
|
379 |
+
try {
|
380 |
+
// 首先尝试使用Puppeteer
|
381 |
+
return await this.generateScreenshotWithPuppeteer(htmlContent, options);
|
382 |
+
} catch (puppeteerError) {
|
383 |
+
console.warn('Puppeteer截图失败,使用fallback方法:', puppeteerError.message);
|
384 |
+
|
385 |
+
// 如果Puppeteer失败,生成fallback图片
|
386 |
+
const dimensions = this.extractPPTDimensions(htmlContent);
|
387 |
+
const fallbackImage = this.generateFallbackImage(
|
388 |
+
dimensions.width,
|
389 |
+
dimensions.height,
|
390 |
+
'PPT Preview'
|
391 |
+
);
|
392 |
+
|
393 |
+
console.log(`生成fallback图片,尺寸: ${dimensions.width}x${dimensions.height}`);
|
394 |
+
return fallbackImage;
|
395 |
+
}
|
396 |
}
|
397 |
|
398 |
// 兼容旧方法名
|