Upload screenshotService.js
Browse files
backend/src/services/screenshotService.js
CHANGED
@@ -25,8 +25,8 @@ class ScreenshotService {
|
|
25 |
|
26 |
const launchOptions = {
|
27 |
headless: 'new',
|
28 |
-
timeout:
|
29 |
-
protocolTimeout:
|
30 |
args: [
|
31 |
'--no-sandbox',
|
32 |
'--disable-setuid-sandbox',
|
@@ -55,7 +55,7 @@ class ScreenshotService {
|
|
55 |
'--disable-hang-monitor',
|
56 |
'--disable-prompt-on-repost',
|
57 |
'--memory-pressure-off',
|
58 |
-
'--max_old_space_size=
|
59 |
]
|
60 |
};
|
61 |
|
@@ -72,6 +72,7 @@ class ScreenshotService {
|
|
72 |
}
|
73 |
|
74 |
this.browser = await puppeteer.launch(launchOptions);
|
|
|
75 |
|
76 |
// 监听浏览器断开连接事件
|
77 |
this.browser.on('disconnected', () => {
|
@@ -80,19 +81,18 @@ class ScreenshotService {
|
|
80 |
this.isClosing = false;
|
81 |
});
|
82 |
|
83 |
-
console.log('Puppeteer浏览器初始化完成');
|
84 |
this.browserLaunchRetries = 0;
|
85 |
} catch (error) {
|
86 |
-
console.error('Puppeteer浏览器初始化失败:', error);
|
87 |
this.browserLaunchRetries++;
|
88 |
|
89 |
if (this.browserLaunchRetries <= this.maxBrowserLaunchRetries) {
|
90 |
console.log(`尝试重新初始化浏览器 (${this.browserLaunchRetries}/${this.maxBrowserLaunchRetries})`);
|
91 |
-
await this.delay(
|
92 |
return this.initBrowser();
|
93 |
} else {
|
94 |
// 如果Puppeteer完全失败,返回null,使用fallback方法
|
95 |
-
console.warn('Puppeteer初始化完全失败,将使用fallback方法');
|
96 |
return null;
|
97 |
}
|
98 |
}
|
@@ -102,6 +102,7 @@ class ScreenshotService {
|
|
102 |
if (this.browser) {
|
103 |
try {
|
104 |
await this.browser.version();
|
|
|
105 |
} catch (error) {
|
106 |
console.warn('浏览器连接已断开,重新初始化:', error.message);
|
107 |
this.browser = null;
|
@@ -164,25 +165,32 @@ class ScreenshotService {
|
|
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="#
|
|
|
|
|
|
|
|
|
|
|
|
|
171 |
<text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle"
|
172 |
-
font-family="Arial, sans-serif" font-size="
|
173 |
${message}
|
174 |
</text>
|
175 |
<text x="50%" y="60%" text-anchor="middle" dominant-baseline="middle"
|
176 |
-
font-family="Arial, sans-serif" font-size="
|
177 |
-
|
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 |
}
|
@@ -286,16 +294,16 @@ class ScreenshotService {
|
|
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);
|
298 |
-
console.log(
|
299 |
|
300 |
// 优化HTML内容以确保精确截图
|
301 |
const optimizedHtml = this.optimizeHtmlForScreenshot(htmlContent, dimensions.width, dimensions.height);
|
@@ -304,31 +312,36 @@ class ScreenshotService {
|
|
304 |
let page = null;
|
305 |
try {
|
306 |
page = await browser.newPage();
|
|
|
307 |
|
308 |
// 设置页面超时
|
309 |
-
page.setDefaultTimeout(
|
310 |
-
page.setDefaultNavigationTimeout(
|
311 |
|
312 |
// 设置精确的viewport尺寸
|
313 |
await page.setViewport({
|
314 |
width: dimensions.width,
|
315 |
height: dimensions.height,
|
316 |
-
deviceScaleFactor:
|
317 |
});
|
|
|
318 |
|
319 |
// 设置页面内容
|
320 |
await page.setContent(optimizedHtml, {
|
321 |
waitUntil: ['load', 'domcontentloaded'],
|
322 |
-
timeout:
|
323 |
});
|
|
|
324 |
|
325 |
// 等待页面完全渲染
|
326 |
-
await page.waitForTimeout(
|
|
|
327 |
|
328 |
// 执行截图,使用精确的剪裁区域
|
|
|
329 |
const screenshot = await page.screenshot({
|
330 |
-
type:
|
331 |
-
quality:
|
332 |
clip: {
|
333 |
x: 0,
|
334 |
y: 0,
|
@@ -339,13 +352,14 @@ class ScreenshotService {
|
|
339 |
captureBeyondViewport: false,
|
340 |
});
|
341 |
|
342 |
-
console.log(
|
343 |
return screenshot;
|
344 |
|
345 |
} finally {
|
346 |
if (page) {
|
347 |
try {
|
348 |
await page.close();
|
|
|
349 |
} catch (error) {
|
350 |
console.warn('关闭页面时出错:', error.message);
|
351 |
}
|
@@ -353,21 +367,22 @@ class ScreenshotService {
|
|
353 |
}
|
354 |
|
355 |
} catch (error) {
|
356 |
-
console.error(
|
357 |
|
358 |
// 如果是目标关闭错误或连接断开,重置浏览器
|
359 |
if (error.message.includes('Target closed') ||
|
360 |
error.message.includes('Connection closed') ||
|
361 |
error.message.includes('Protocol error')) {
|
362 |
-
console.log('检测到浏览器连接问题,重置浏览器实例');
|
363 |
this.browser = null;
|
364 |
this.isClosing = false;
|
365 |
}
|
366 |
|
367 |
// 如果还有重试机会,进行重试
|
368 |
if (retryCount < this.maxRetries) {
|
369 |
-
|
370 |
-
|
|
|
371 |
return this.generateScreenshotWithPuppeteer(htmlContent, options, retryCount + 1);
|
372 |
}
|
373 |
|
@@ -376,21 +391,26 @@ class ScreenshotService {
|
|
376 |
}
|
377 |
|
378 |
async generateScreenshot(htmlContent, options = {}) {
|
|
|
|
|
379 |
try {
|
380 |
// 首先尝试使用Puppeteer
|
381 |
-
|
|
|
|
|
|
|
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 |
-
'
|
391 |
);
|
392 |
|
393 |
-
console.log(
|
394 |
return fallbackImage;
|
395 |
}
|
396 |
}
|
|
|
25 |
|
26 |
const launchOptions = {
|
27 |
headless: 'new',
|
28 |
+
timeout: 30000, // 减少超时时间
|
29 |
+
protocolTimeout: 30000,
|
30 |
args: [
|
31 |
'--no-sandbox',
|
32 |
'--disable-setuid-sandbox',
|
|
|
55 |
'--disable-hang-monitor',
|
56 |
'--disable-prompt-on-repost',
|
57 |
'--memory-pressure-off',
|
58 |
+
'--max_old_space_size=512' // 减少内存使用
|
59 |
]
|
60 |
};
|
61 |
|
|
|
72 |
}
|
73 |
|
74 |
this.browser = await puppeteer.launch(launchOptions);
|
75 |
+
console.log('✅ Puppeteer浏览器初始化成功');
|
76 |
|
77 |
// 监听浏览器断开连接事件
|
78 |
this.browser.on('disconnected', () => {
|
|
|
81 |
this.isClosing = false;
|
82 |
});
|
83 |
|
|
|
84 |
this.browserLaunchRetries = 0;
|
85 |
} catch (error) {
|
86 |
+
console.error('❌ Puppeteer浏览器初始化失败:', error.message);
|
87 |
this.browserLaunchRetries++;
|
88 |
|
89 |
if (this.browserLaunchRetries <= this.maxBrowserLaunchRetries) {
|
90 |
console.log(`尝试重新初始化浏览器 (${this.browserLaunchRetries}/${this.maxBrowserLaunchRetries})`);
|
91 |
+
await this.delay(2000); // 等待2秒后重试
|
92 |
return this.initBrowser();
|
93 |
} else {
|
94 |
// 如果Puppeteer完全失败,返回null,使用fallback方法
|
95 |
+
console.warn('⚠️ Puppeteer初始化完全失败,将使用fallback方法');
|
96 |
return null;
|
97 |
}
|
98 |
}
|
|
|
102 |
if (this.browser) {
|
103 |
try {
|
104 |
await this.browser.version();
|
105 |
+
console.log('✅ 浏览器连接正常');
|
106 |
} catch (error) {
|
107 |
console.warn('浏览器连接已断开,重新初始化:', error.message);
|
108 |
this.browser = null;
|
|
|
165 |
|
166 |
// 生成简化的PNG图片作为fallback
|
167 |
generateFallbackImage(width, height, message = 'Screenshot not available') {
|
168 |
+
console.log(`🔄 生成fallback图片: ${width}x${height}`);
|
169 |
+
|
170 |
// 创建一个简单的SVG作为fallback
|
171 |
const svg = `
|
172 |
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
173 |
+
<rect width="100%" height="100%" fill="#f8f9fa"/>
|
174 |
+
<rect x="10" y="10" width="${width-20}" height="${height-20}"
|
175 |
+
fill="none" stroke="#dee2e6" stroke-width="2" stroke-dasharray="10,5"/>
|
176 |
+
<text x="50%" y="40%" text-anchor="middle" dominant-baseline="middle"
|
177 |
+
font-family="Arial, sans-serif" font-size="28" fill="#495057" font-weight="bold">
|
178 |
+
PPT 预览图
|
179 |
+
</text>
|
180 |
<text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle"
|
181 |
+
font-family="Arial, sans-serif" font-size="16" fill="#6c757d">
|
182 |
${message}
|
183 |
</text>
|
184 |
<text x="50%" y="60%" text-anchor="middle" dominant-baseline="middle"
|
185 |
+
font-family="Arial, sans-serif" font-size="14" fill="#adb5bd">
|
186 |
+
尺寸: ${width} × ${height}
|
187 |
</text>
|
188 |
+
<circle cx="50%" cy="70%" r="20" fill="none" stroke="#28a745" stroke-width="3">
|
189 |
+
<animate attributeName="stroke-dasharray" values="0,126;126,126" dur="2s" repeatCount="indefinite"/>
|
190 |
+
</circle>
|
191 |
</svg>
|
192 |
`;
|
193 |
|
|
|
|
|
|
|
|
|
194 |
// 返回简单的占位图片数据
|
195 |
return Buffer.from(svg, 'utf8');
|
196 |
}
|
|
|
294 |
|
295 |
async generateScreenshotWithPuppeteer(htmlContent, options = {}, retryCount = 0) {
|
296 |
try {
|
297 |
+
console.log(`🎯 开始Puppeteer截图生成... (尝试 ${retryCount + 1}/${this.maxRetries + 1})`);
|
298 |
+
|
299 |
const browser = await this.initBrowser();
|
300 |
if (!browser) {
|
301 |
throw new Error('浏览器初始化失败');
|
302 |
}
|
303 |
|
|
|
|
|
304 |
// 从HTML中提取PPT的精确尺寸
|
305 |
const dimensions = this.extractPPTDimensions(htmlContent);
|
306 |
+
console.log(`📐 检测到PPT尺寸: ${dimensions.width}x${dimensions.height}`);
|
307 |
|
308 |
// 优化HTML内容以确保精确截图
|
309 |
const optimizedHtml = this.optimizeHtmlForScreenshot(htmlContent, dimensions.width, dimensions.height);
|
|
|
312 |
let page = null;
|
313 |
try {
|
314 |
page = await browser.newPage();
|
315 |
+
console.log('📄 创建新页面成功');
|
316 |
|
317 |
// 设置页面超时
|
318 |
+
page.setDefaultTimeout(20000); // 减少超时时间
|
319 |
+
page.setDefaultNavigationTimeout(20000);
|
320 |
|
321 |
// 设置精确的viewport尺寸
|
322 |
await page.setViewport({
|
323 |
width: dimensions.width,
|
324 |
height: dimensions.height,
|
325 |
+
deviceScaleFactor: 1, // 固定为1避免缩放问题
|
326 |
});
|
327 |
+
console.log(`🖥️ 设置viewport: ${dimensions.width}x${dimensions.height}`);
|
328 |
|
329 |
// 设置页面内容
|
330 |
await page.setContent(optimizedHtml, {
|
331 |
waitUntil: ['load', 'domcontentloaded'],
|
332 |
+
timeout: 20000
|
333 |
});
|
334 |
+
console.log('📝 页面内容设置完成');
|
335 |
|
336 |
// 等待页面完全渲染
|
337 |
+
await page.waitForTimeout(1000); // 减少等待时间
|
338 |
+
console.log('⏱️ 等待渲染完成');
|
339 |
|
340 |
// 执行截图,使用精确的剪裁区域
|
341 |
+
console.log('📸 开始执行截图...');
|
342 |
const screenshot = await page.screenshot({
|
343 |
+
type: 'jpeg',
|
344 |
+
quality: 85, // 稍微降低质量提高速度
|
345 |
clip: {
|
346 |
x: 0,
|
347 |
y: 0,
|
|
|
352 |
captureBeyondViewport: false,
|
353 |
});
|
354 |
|
355 |
+
console.log(`✅ Puppeteer截图成功生成,尺寸: ${dimensions.width}x${dimensions.height}, 数据大小: ${screenshot.length} 字节`);
|
356 |
return screenshot;
|
357 |
|
358 |
} finally {
|
359 |
if (page) {
|
360 |
try {
|
361 |
await page.close();
|
362 |
+
console.log('📄 页面已关闭');
|
363 |
} catch (error) {
|
364 |
console.warn('关闭页面时出错:', error.message);
|
365 |
}
|
|
|
367 |
}
|
368 |
|
369 |
} catch (error) {
|
370 |
+
console.error(`❌ Puppeteer截图生成失败 (尝试 ${retryCount + 1}):`, error.message);
|
371 |
|
372 |
// 如果是目标关闭错误或连接断开,重置浏览器
|
373 |
if (error.message.includes('Target closed') ||
|
374 |
error.message.includes('Connection closed') ||
|
375 |
error.message.includes('Protocol error')) {
|
376 |
+
console.log('🔄 检测到浏览器连接问题,重置浏览器实例');
|
377 |
this.browser = null;
|
378 |
this.isClosing = false;
|
379 |
}
|
380 |
|
381 |
// 如果还有重试机会,进行重试
|
382 |
if (retryCount < this.maxRetries) {
|
383 |
+
const waitTime = (retryCount + 1) * 2;
|
384 |
+
console.log(`⏳ 等待 ${waitTime} 秒后重试...`);
|
385 |
+
await this.delay(waitTime * 1000);
|
386 |
return this.generateScreenshotWithPuppeteer(htmlContent, options, retryCount + 1);
|
387 |
}
|
388 |
|
|
|
391 |
}
|
392 |
|
393 |
async generateScreenshot(htmlContent, options = {}) {
|
394 |
+
console.log('🎯 开始生成截图...');
|
395 |
+
|
396 |
try {
|
397 |
// 首先尝试使用Puppeteer
|
398 |
+
console.log('🚀 尝试使用Puppeteer生成截图');
|
399 |
+
const screenshot = await this.generateScreenshotWithPuppeteer(htmlContent, options);
|
400 |
+
console.log('✅ Puppeteer截图生成成功');
|
401 |
+
return screenshot;
|
402 |
} catch (puppeteerError) {
|
403 |
+
console.warn('⚠️ Puppeteer截图失败,使用fallback方法:', puppeteerError.message);
|
404 |
|
405 |
// 如果Puppeteer失败,生成fallback图片
|
406 |
const dimensions = this.extractPPTDimensions(htmlContent);
|
407 |
const fallbackImage = this.generateFallbackImage(
|
408 |
dimensions.width,
|
409 |
dimensions.height,
|
410 |
+
'Puppeteer不可用,��示占位图'
|
411 |
);
|
412 |
|
413 |
+
console.log(`📋 生成fallback图片,尺寸: ${dimensions.width}x${dimensions.height}`);
|
414 |
return fallbackImage;
|
415 |
}
|
416 |
}
|