Upload screenshotService.js
Browse files
backend/src/services/screenshotService.js
CHANGED
@@ -1,19 +1,15 @@
|
|
1 |
import puppeteer from 'puppeteer';
|
2 |
|
3 |
class ScreenshotService {
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
console.log('Extracted PPT dimensions:', pptDimensions);
|
14 |
-
|
15 |
-
// 更适合容器环境的Puppeteer配置
|
16 |
-
browser = await puppeteer.launch({
|
17 |
headless: 'new',
|
18 |
args: [
|
19 |
'--no-sandbox',
|
@@ -22,333 +18,406 @@ class ScreenshotService {
|
|
22 |
'--disable-accelerated-2d-canvas',
|
23 |
'--no-first-run',
|
24 |
'--no-zygote',
|
|
|
25 |
'--disable-gpu',
|
26 |
'--disable-background-timer-throttling',
|
27 |
'--disable-backgrounding-occluded-windows',
|
28 |
'--disable-renderer-backgrounding',
|
29 |
-
'--disable-web-security',
|
30 |
'--disable-features=TranslateUI',
|
31 |
'--disable-extensions',
|
32 |
-
'--
|
33 |
-
'--disable-default-apps',
|
34 |
'--mute-audio',
|
35 |
'--no-default-browser-check',
|
36 |
-
'--
|
37 |
-
'--disable-background-
|
38 |
-
'--disable-
|
39 |
-
'--
|
40 |
-
'--disable-
|
41 |
-
'--
|
42 |
-
'--
|
43 |
-
|
44 |
-
|
45 |
-
protocolTimeout: 30000
|
46 |
-
});
|
47 |
-
|
48 |
-
console.log('Browser launched successfully');
|
49 |
-
|
50 |
-
const page = await browser.newPage();
|
51 |
-
console.log('New page created');
|
52 |
-
|
53 |
-
// 设置页面视窗大小为PPT的精确尺寸
|
54 |
-
await page.setViewport({
|
55 |
-
width: pptDimensions.width,
|
56 |
-
height: pptDimensions.height,
|
57 |
-
deviceScaleFactor: 2 // 高分辨率
|
58 |
-
});
|
59 |
-
|
60 |
-
console.log(`Viewport set to exact PPT size: ${pptDimensions.width}x${pptDimensions.height}`);
|
61 |
-
|
62 |
-
// 修改HTML内容,确保PPT容器精确匹配视窗尺寸,消除白边
|
63 |
-
const modifiedHtmlContent = this.modifyHtmlForScreenshot(htmlContent, pptDimensions);
|
64 |
-
|
65 |
-
// 设置HTML内容
|
66 |
-
await page.setContent(modifiedHtmlContent, {
|
67 |
-
waitUntil: 'domcontentloaded',
|
68 |
-
timeout: 10000
|
69 |
-
});
|
70 |
-
|
71 |
-
console.log('HTML content set');
|
72 |
-
|
73 |
-
// 等待渲染完成
|
74 |
-
await page.waitForTimeout(1500);
|
75 |
-
|
76 |
-
// 验证页面中的PPT容器尺寸是否精确匹配
|
77 |
-
const actualDimensions = await page.evaluate(() => {
|
78 |
-
const container = document.getElementById('slideContainer');
|
79 |
-
const body = document.body;
|
80 |
-
const html = document.documentElement;
|
81 |
-
|
82 |
-
if (container) {
|
83 |
-
const rect = container.getBoundingClientRect();
|
84 |
-
return {
|
85 |
-
container: {
|
86 |
-
width: rect.width,
|
87 |
-
height: rect.height,
|
88 |
-
left: rect.left,
|
89 |
-
top: rect.top
|
90 |
-
},
|
91 |
-
body: {
|
92 |
-
width: body.offsetWidth,
|
93 |
-
height: body.offsetHeight
|
94 |
-
},
|
95 |
-
html: {
|
96 |
-
width: html.offsetWidth,
|
97 |
-
height: html.offsetHeight
|
98 |
-
},
|
99 |
-
viewport: {
|
100 |
-
width: window.innerWidth,
|
101 |
-
height: window.innerHeight
|
102 |
-
}
|
103 |
-
};
|
104 |
-
}
|
105 |
-
return null;
|
106 |
-
});
|
107 |
-
|
108 |
-
console.log('Page dimensions verification:', actualDimensions);
|
109 |
-
|
110 |
-
// 确保页面尺寸完全匹配PPT尺寸
|
111 |
-
if (actualDimensions && (
|
112 |
-
Math.abs(actualDimensions.viewport.width - pptDimensions.width) > 1 ||
|
113 |
-
Math.abs(actualDimensions.viewport.height - pptDimensions.height) > 1
|
114 |
-
)) {
|
115 |
-
console.warn('Page dimensions do not match PPT dimensions exactly, adjusting...');
|
116 |
-
await page.setViewport({
|
117 |
-
width: pptDimensions.width,
|
118 |
-
height: pptDimensions.height,
|
119 |
-
deviceScaleFactor: 2
|
120 |
-
});
|
121 |
-
await page.waitForTimeout(500);
|
122 |
-
}
|
123 |
-
|
124 |
-
console.log('Taking precise screenshot...');
|
125 |
-
|
126 |
-
// 截取精确的PPT内容区域,不包含任何白边
|
127 |
-
const screenshot = await page.screenshot({
|
128 |
-
type: 'jpeg',
|
129 |
-
quality: 95,
|
130 |
-
// 使用clip精确截取PPT内容区域
|
131 |
-
clip: {
|
132 |
-
x: 0,
|
133 |
-
y: 0,
|
134 |
-
width: pptDimensions.width,
|
135 |
-
height: pptDimensions.height
|
136 |
-
},
|
137 |
-
timeout: 10000
|
138 |
});
|
139 |
-
|
140 |
-
console.log('Screenshot taken successfully, size:', screenshot.length);
|
141 |
-
|
142 |
-
return screenshot;
|
143 |
-
} catch (error) {
|
144 |
-
console.error('Screenshot generation error:', error);
|
145 |
-
|
146 |
-
// 返回一个错误图片而不是抛出异常
|
147 |
-
return this.generateErrorImage(error.message, pptDimensions);
|
148 |
-
} finally {
|
149 |
-
if (browser) {
|
150 |
-
try {
|
151 |
-
await browser.close();
|
152 |
-
console.log('Browser closed');
|
153 |
-
} catch (closeError) {
|
154 |
-
console.error('Error closing browser:', closeError);
|
155 |
-
}
|
156 |
-
}
|
157 |
}
|
|
|
158 |
}
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
const width = parseInt(dimensionsMatch[1]);
|
166 |
-
const height = parseInt(dimensionsMatch[2]);
|
167 |
-
console.log('Extracted dimensions from JavaScript:', { width, height });
|
168 |
-
return { width, height };
|
169 |
-
}
|
170 |
-
|
171 |
-
// 方法2:从CSS样式中提取尺寸
|
172 |
-
const cssMatch = htmlContent.match(/\.slide-container\s*{\s*[^}]*width:\s*(\d+)px;[^}]*height:\s*(\d+)px;/s);
|
173 |
-
if (cssMatch) {
|
174 |
-
const width = parseInt(cssMatch[1]);
|
175 |
-
const height = parseInt(cssMatch[2]);
|
176 |
-
console.log('Extracted dimensions from CSS:', { width, height });
|
177 |
-
return { width, height };
|
178 |
-
}
|
179 |
-
|
180 |
-
// 方法3:从内联样式中提取尺寸
|
181 |
-
const inlineMatch = htmlContent.match(/width:\s*(\d+)px[^;]*;\s*height:\s*(\d+)px/);
|
182 |
-
if (inlineMatch) {
|
183 |
-
const width = parseInt(inlineMatch[1]);
|
184 |
-
const height = parseInt(inlineMatch[2]);
|
185 |
-
console.log('Extracted dimensions from inline styles:', { width, height });
|
186 |
-
return { width, height };
|
187 |
}
|
188 |
-
|
189 |
-
// 默认尺寸 - 使用标准PPT 16:9 比例
|
190 |
-
console.warn('Could not extract PPT dimensions from HTML, using default 16:9 ratio');
|
191 |
-
return { width: 1280, height: 720 }; // 16:9 标准比例
|
192 |
}
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
const { width, height } = pptDimensions;
|
197 |
-
|
198 |
-
// 创建完全匹配PPT尺寸的HTML,消除所有可能的白边
|
199 |
-
let modifiedHtml = htmlContent;
|
200 |
-
|
201 |
-
// 设置严格的CSS样式,确保页面尺寸完全匹配PPT尺寸
|
202 |
-
modifiedHtml = modifiedHtml.replace(
|
203 |
-
/<style>[\s\S]*?<\/style>/,
|
204 |
-
`<style>
|
205 |
-
* {
|
206 |
-
margin: 0 !important;
|
207 |
-
padding: 0 !important;
|
208 |
-
box-sizing: border-box !important;
|
209 |
-
border: none !important;
|
210 |
-
}
|
211 |
-
|
212 |
-
html {
|
213 |
-
width: ${width}px !important;
|
214 |
-
height: ${height}px !important;
|
215 |
-
overflow: hidden !important;
|
216 |
-
background: transparent !important;
|
217 |
-
min-width: ${width}px !important;
|
218 |
-
min-height: ${height}px !important;
|
219 |
-
max-width: ${width}px !important;
|
220 |
-
max-height: ${height}px !important;
|
221 |
-
}
|
222 |
-
|
223 |
-
body {
|
224 |
-
width: ${width}px !important;
|
225 |
-
height: ${height}px !important;
|
226 |
-
overflow: hidden !important;
|
227 |
-
font-family: 'Microsoft YaHei', Arial, sans-serif !important;
|
228 |
-
background: transparent !important;
|
229 |
-
position: absolute !important;
|
230 |
-
top: 0 !important;
|
231 |
-
left: 0 !important;
|
232 |
-
margin: 0 !important;
|
233 |
-
padding: 0 !important;
|
234 |
-
min-width: ${width}px !important;
|
235 |
-
min-height: ${height}px !important;
|
236 |
-
max-width: ${width}px !important;
|
237 |
-
max-height: ${height}px !important;
|
238 |
-
}
|
239 |
-
|
240 |
-
.slide-container {
|
241 |
-
width: ${width}px !important;
|
242 |
-
height: ${height}px !important;
|
243 |
-
position: absolute !important;
|
244 |
-
top: 0 !important;
|
245 |
-
left: 0 !important;
|
246 |
-
margin: 0 !important;
|
247 |
-
padding: 0 !important;
|
248 |
-
overflow: hidden !important;
|
249 |
-
background-color: var(--slide-bg-color, #ffffff) !important;
|
250 |
-
transform: none !important;
|
251 |
-
transform-origin: top left !important;
|
252 |
-
min-width: ${width}px !important;
|
253 |
-
min-height: ${height}px !important;
|
254 |
-
max-width: ${width}px !important;
|
255 |
-
max-height: ${height}px !important;
|
256 |
-
}
|
257 |
-
|
258 |
-
/* 确保所有元素都相对于slide-container定位 */
|
259 |
-
.slide-container > * {
|
260 |
-
position: absolute !important;
|
261 |
-
}
|
262 |
-
|
263 |
-
/* 确保背景图片也严格匹配尺寸 */
|
264 |
-
.slide-container::before {
|
265 |
-
width: ${width}px !important;
|
266 |
-
height: ${height}px !important;
|
267 |
-
top: 0 !important;
|
268 |
-
left: 0 !important;
|
269 |
-
margin: 0 !important;
|
270 |
-
padding: 0 !important;
|
271 |
-
}
|
272 |
-
</style>`
|
273 |
-
);
|
274 |
|
275 |
-
//
|
276 |
-
|
277 |
-
/<
|
278 |
-
`<
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
left: 0 !important;
|
293 |
-
margin: 0 !important;
|
294 |
-
padding: 0 !important;
|
295 |
-
transform: none !important;
|
296 |
-
overflow: hidden !important;
|
297 |
-
\`;
|
298 |
}
|
299 |
|
300 |
-
|
301 |
-
|
302 |
-
width: ${
|
303 |
-
height: ${
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
304 |
margin: 0 !important;
|
305 |
padding: 0 !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
306 |
overflow: hidden !important;
|
307 |
-
position:
|
308 |
top: 0 !important;
|
309 |
left: 0 !important;
|
310 |
-
\`;
|
311 |
-
|
312 |
-
document.documentElement.style.cssText = \`
|
313 |
-
width: ${width}px !important;
|
314 |
-
height: ${height}px !important;
|
315 |
margin: 0 !important;
|
316 |
padding: 0 !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
317 |
overflow: hidden !important;
|
318 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
319 |
|
320 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
321 |
});
|
322 |
|
323 |
-
//
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
});
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
352 |
}
|
353 |
}
|
354 |
|
|
|
1 |
import puppeteer from 'puppeteer';
|
2 |
|
3 |
class ScreenshotService {
|
4 |
+
constructor() {
|
5 |
+
this.browser = null;
|
6 |
+
this.isInitialized = false;
|
7 |
+
}
|
8 |
+
|
9 |
+
async initBrowser() {
|
10 |
+
if (!this.browser) {
|
11 |
+
console.log('初始化Puppeteer浏览器...');
|
12 |
+
this.browser = await puppeteer.launch({
|
|
|
|
|
|
|
|
|
13 |
headless: 'new',
|
14 |
args: [
|
15 |
'--no-sandbox',
|
|
|
18 |
'--disable-accelerated-2d-canvas',
|
19 |
'--no-first-run',
|
20 |
'--no-zygote',
|
21 |
+
'--single-process',
|
22 |
'--disable-gpu',
|
23 |
'--disable-background-timer-throttling',
|
24 |
'--disable-backgrounding-occluded-windows',
|
25 |
'--disable-renderer-backgrounding',
|
|
|
26 |
'--disable-features=TranslateUI',
|
27 |
'--disable-extensions',
|
28 |
+
'--hide-scrollbars',
|
|
|
29 |
'--mute-audio',
|
30 |
'--no-default-browser-check',
|
31 |
+
'--disable-default-apps',
|
32 |
+
'--disable-background-networking',
|
33 |
+
'--disable-sync',
|
34 |
+
'--metrics-recording-only',
|
35 |
+
'--disable-domain-reliability',
|
36 |
+
'--disable-component-extensions-with-background-pages',
|
37 |
+
'--force-device-scale-factor=1',
|
38 |
+
'--enable-precise-memory-info'
|
39 |
+
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
40 |
});
|
41 |
+
console.log('Puppeteer浏览器初始化完成');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
}
|
43 |
+
return this.browser;
|
44 |
}
|
45 |
+
|
46 |
+
async closeBrowser() {
|
47 |
+
if (this.browser) {
|
48 |
+
await this.browser.close();
|
49 |
+
this.browser = null;
|
50 |
+
console.log('Puppeteer浏览器已关闭');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
}
|
|
|
|
|
|
|
|
|
52 |
}
|
53 |
+
|
54 |
+
async modifyHtmlForScreenshot(htmlContent, targetWidth, targetHeight) {
|
55 |
+
console.log(`开始修改HTML for截图, 目标尺寸: ${targetWidth}x${targetHeight}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
56 |
|
57 |
+
// 创建完全针对截图优化的HTML版本
|
58 |
+
const optimizedHtml = htmlContent.replace(
|
59 |
+
/<head>/i,
|
60 |
+
`<head>
|
61 |
+
<meta charset="UTF-8">
|
62 |
+
<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">
|
63 |
+
<!-- 截图模式标识 -->
|
64 |
+
<meta name="screenshot-mode" content="true">
|
65 |
+
<!-- 强制精确尺寸控制 -->
|
66 |
+
<style id="screenshot-optimization">
|
67 |
+
/* 全局重置 - 完全消除边距 */
|
68 |
+
*, *::before, *::after {
|
69 |
+
margin: 0 !important;
|
70 |
+
padding: 0 !important;
|
71 |
+
box-sizing: border-box !important;
|
72 |
+
border: none !important;
|
73 |
+
outline: none !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
74 |
}
|
75 |
|
76 |
+
/* 根元素强制设置 */
|
77 |
+
html {
|
78 |
+
width: ${targetWidth}px !important;
|
79 |
+
height: ${targetHeight}px !important;
|
80 |
+
min-width: ${targetWidth}px !important;
|
81 |
+
min-height: ${targetHeight}px !important;
|
82 |
+
max-width: ${targetWidth}px !important;
|
83 |
+
max-height: ${targetHeight}px !important;
|
84 |
+
overflow: hidden !important;
|
85 |
+
position: fixed !important;
|
86 |
+
top: 0 !important;
|
87 |
+
left: 0 !important;
|
88 |
margin: 0 !important;
|
89 |
padding: 0 !important;
|
90 |
+
border: none !important;
|
91 |
+
outline: none !important;
|
92 |
+
transform: none !important;
|
93 |
+
transform-origin: top left !important;
|
94 |
+
}
|
95 |
+
|
96 |
+
/* Body元素强制设置 */
|
97 |
+
body {
|
98 |
+
width: ${targetWidth}px !important;
|
99 |
+
height: ${targetHeight}px !important;
|
100 |
+
min-width: ${targetWidth}px !important;
|
101 |
+
min-height: ${targetHeight}px !important;
|
102 |
+
max-width: ${targetWidth}px !important;
|
103 |
+
max-height: ${targetHeight}px !important;
|
104 |
overflow: hidden !important;
|
105 |
+
position: fixed !important;
|
106 |
top: 0 !important;
|
107 |
left: 0 !important;
|
|
|
|
|
|
|
|
|
|
|
108 |
margin: 0 !important;
|
109 |
padding: 0 !important;
|
110 |
+
border: none !important;
|
111 |
+
outline: none !important;
|
112 |
+
transform: none !important;
|
113 |
+
transform-origin: top left !important;
|
114 |
+
}
|
115 |
+
|
116 |
+
/* 幻灯片容器强制设置 */
|
117 |
+
.slide-container {
|
118 |
+
width: ${targetWidth}px !important;
|
119 |
+
height: ${targetHeight}px !important;
|
120 |
+
min-width: ${targetWidth}px !important;
|
121 |
+
min-height: ${targetHeight}px !important;
|
122 |
+
max-width: ${targetWidth}px !important;
|
123 |
+
max-height: ${targetHeight}px !important;
|
124 |
+
position: fixed !important;
|
125 |
+
top: 0 !important;
|
126 |
+
left: 0 !important;
|
127 |
overflow: hidden !important;
|
128 |
+
transform: none !important;
|
129 |
+
transform-origin: top left !important;
|
130 |
+
margin: 0 !important;
|
131 |
+
padding: 0 !important;
|
132 |
+
border: none !important;
|
133 |
+
outline: none !important;
|
134 |
+
box-shadow: none !important;
|
135 |
+
z-index: 1 !important;
|
136 |
+
}
|
137 |
+
|
138 |
+
/* 隐藏所有滚动条 */
|
139 |
+
html::-webkit-scrollbar,
|
140 |
+
body::-webkit-scrollbar,
|
141 |
+
*::-webkit-scrollbar {
|
142 |
+
display: none !important;
|
143 |
+
width: 0 !important;
|
144 |
+
height: 0 !important;
|
145 |
+
}
|
146 |
+
|
147 |
+
/* Firefox */
|
148 |
+
html {
|
149 |
+
scrollbar-width: none !important;
|
150 |
+
}
|
151 |
+
|
152 |
+
/* 禁用用户交互 */
|
153 |
+
* {
|
154 |
+
-webkit-user-select: none !important;
|
155 |
+
-moz-user-select: none !important;
|
156 |
+
-ms-user-select: none !important;
|
157 |
+
user-select: none !important;
|
158 |
+
-webkit-user-drag: none !important;
|
159 |
+
-khtml-user-drag: none !important;
|
160 |
+
-moz-user-drag: none !important;
|
161 |
+
-o-user-drag: none !important;
|
162 |
+
user-drag: none !important;
|
163 |
+
pointer-events: none !important;
|
164 |
+
}
|
165 |
+
|
166 |
+
/* 移除响应式样式 */
|
167 |
+
.browse-mode * {
|
168 |
+
display: none !important;
|
169 |
+
}
|
170 |
+
</style>`
|
171 |
+
);
|
172 |
+
|
173 |
+
// 注入截图专用JavaScript
|
174 |
+
const finalHtml = optimizedHtml.replace(
|
175 |
+
/<\/body>/i,
|
176 |
+
`
|
177 |
+
<script id="screenshot-finalizer">
|
178 |
+
// 截图模式最终设置
|
179 |
+
window.SCREENSHOT_EXACT_MODE = true;
|
180 |
+
window.PPT_TARGET_WIDTH = ${targetWidth};
|
181 |
+
window.PPT_TARGET_HEIGHT = ${targetHeight};
|
182 |
+
|
183 |
+
// 立即执行尺寸强制设置
|
184 |
+
(function() {
|
185 |
+
const exactWidth = ${targetWidth};
|
186 |
+
const exactHeight = ${targetHeight};
|
187 |
+
|
188 |
+
console.log('截图模式强制设置:', exactWidth + 'x' + exactHeight);
|
189 |
+
|
190 |
+
// 强制设置根元素
|
191 |
+
const html = document.documentElement;
|
192 |
+
const body = document.body;
|
193 |
+
|
194 |
+
html.style.cssText = \`
|
195 |
+
width: \${exactWidth}px !important;
|
196 |
+
height: \${exactHeight}px !important;
|
197 |
+
min-width: \${exactWidth}px !important;
|
198 |
+
min-height: \${exactHeight}px !important;
|
199 |
+
max-width: \${exactWidth}px !important;
|
200 |
+
max-height: \${exactHeight}px !important;
|
201 |
+
overflow: hidden !important;
|
202 |
+
margin: 0 !important;
|
203 |
+
padding: 0 !important;
|
204 |
+
position: fixed !important;
|
205 |
+
top: 0 !important;
|
206 |
+
left: 0 !important;
|
207 |
+
border: none !important;
|
208 |
+
outline: none !important;
|
209 |
+
transform: none !important;
|
210 |
+
transform-origin: top left !important;
|
211 |
+
\`;
|
212 |
+
|
213 |
+
body.style.cssText = \`
|
214 |
+
width: \${exactWidth}px !important;
|
215 |
+
height: \${exactHeight}px !important;
|
216 |
+
min-width: \${exactWidth}px !important;
|
217 |
+
min-height: \${exactHeight}px !important;
|
218 |
+
max-width: \${exactWidth}px !important;
|
219 |
+
max-height: \${exactHeight}px !important;
|
220 |
+
overflow: hidden !important;
|
221 |
+
margin: 0 !important;
|
222 |
+
padding: 0 !important;
|
223 |
+
position: fixed !important;
|
224 |
+
top: 0 !important;
|
225 |
+
left: 0 !important;
|
226 |
+
border: none !important;
|
227 |
+
outline: none !important;
|
228 |
+
transform: none !important;
|
229 |
+
transform-origin: top left !important;
|
230 |
+
\`;
|
231 |
+
|
232 |
+
// 设置容器
|
233 |
+
const container = document.querySelector('.slide-container');
|
234 |
+
if (container) {
|
235 |
+
container.style.cssText = \`
|
236 |
+
width: \${exactWidth}px !important;
|
237 |
+
height: \${exactHeight}px !important;
|
238 |
+
min-width: \${exactWidth}px !important;
|
239 |
+
min-height: \${exactHeight}px !important;
|
240 |
+
max-width: \${exactWidth}px !important;
|
241 |
+
max-height: \${exactHeight}px !important;
|
242 |
+
position: fixed !important;
|
243 |
+
top: 0 !important;
|
244 |
+
left: 0 !important;
|
245 |
+
overflow: hidden !important;
|
246 |
+
margin: 0 !important;
|
247 |
+
padding: 0 !important;
|
248 |
+
border: none !important;
|
249 |
+
outline: none !important;
|
250 |
+
transform: none !important;
|
251 |
+
box-shadow: none !important;
|
252 |
+
z-index: 1 !important;
|
253 |
+
\`;
|
254 |
+
}
|
255 |
+
|
256 |
+
// 移除响应式类
|
257 |
+
html.classList.remove('browse-mode');
|
258 |
+
body.classList.remove('browse-mode');
|
259 |
+
|
260 |
+
console.log('截图模式设置完成');
|
261 |
+
})();
|
262 |
|
263 |
+
// DOM加载完成后再次确认
|
264 |
+
document.addEventListener('DOMContentLoaded', function() {
|
265 |
+
console.log('DOM加载完成,最终确认尺寸设置');
|
266 |
+
|
267 |
+
// 再次强制设置,确保完全生效
|
268 |
+
const html = document.documentElement;
|
269 |
+
const body = document.body;
|
270 |
+
const container = document.querySelector('.slide-container');
|
271 |
+
|
272 |
+
[html, body, container].forEach(element => {
|
273 |
+
if (element) {
|
274 |
+
element.style.width = '${targetWidth}px';
|
275 |
+
element.style.height = '${targetHeight}px';
|
276 |
+
element.style.minWidth = '${targetWidth}px';
|
277 |
+
element.style.minHeight = '${targetHeight}px';
|
278 |
+
element.style.maxWidth = '${targetWidth}px';
|
279 |
+
element.style.maxHeight = '${targetHeight}px';
|
280 |
+
element.style.margin = '0';
|
281 |
+
element.style.padding = '0';
|
282 |
+
element.style.border = 'none';
|
283 |
+
element.style.outline = 'none';
|
284 |
+
element.style.overflow = 'hidden';
|
285 |
+
|
286 |
+
if (element !== container) {
|
287 |
+
element.style.position = 'fixed';
|
288 |
+
element.style.top = '0';
|
289 |
+
element.style.left = '0';
|
290 |
+
}
|
291 |
+
}
|
292 |
+
});
|
293 |
+
|
294 |
+
// 最终验证
|
295 |
+
setTimeout(() => {
|
296 |
+
console.log('最终尺寸验证:', {
|
297 |
+
html: html.offsetWidth + 'x' + html.offsetHeight,
|
298 |
+
body: body.offsetWidth + 'x' + body.offsetHeight,
|
299 |
+
container: container ? container.offsetWidth + 'x' + container.offsetHeight : 'none',
|
300 |
+
target: '${targetWidth}x${targetHeight}'
|
301 |
+
});
|
302 |
+
}, 100);
|
303 |
+
});
|
304 |
+
</script>
|
305 |
+
</body>`
|
306 |
+
);
|
307 |
+
|
308 |
+
console.log('HTML修改完成,已注入截图优化代码');
|
309 |
+
return finalHtml;
|
310 |
+
}
|
311 |
+
|
312 |
+
async captureScreenshot(htmlContent, width, height, options = {}) {
|
313 |
+
try {
|
314 |
+
await this.initBrowser();
|
315 |
+
|
316 |
+
console.log(`开始截图, 目标尺寸: ${width}x${height}`);
|
317 |
+
|
318 |
+
// 修改HTML内容以适应截图
|
319 |
+
const optimizedHtml = await this.modifyHtmlForScreenshot(htmlContent, width, height);
|
320 |
+
|
321 |
+
// 创建新页面
|
322 |
+
const page = await this.browser.newPage();
|
323 |
+
|
324 |
+
try {
|
325 |
+
// 设置精确的viewport尺寸
|
326 |
+
await page.setViewport({
|
327 |
+
width: width,
|
328 |
+
height: height,
|
329 |
+
deviceScaleFactor: options.deviceScaleFactor || 2, // 高清截图
|
330 |
});
|
331 |
|
332 |
+
// 设置页面内容
|
333 |
+
await page.setContent(optimizedHtml, {
|
334 |
+
waitUntil: ['load', 'domcontentloaded', 'networkidle0'],
|
335 |
+
timeout: 30000
|
336 |
});
|
337 |
+
|
338 |
+
// 等待页面完全渲染
|
339 |
+
await page.waitForTimeout(2000);
|
340 |
+
|
341 |
+
// 执行最终的尺寸验证和调整
|
342 |
+
await page.evaluate((targetWidth, targetHeight) => {
|
343 |
+
const html = document.documentElement;
|
344 |
+
const body = document.body;
|
345 |
+
const container = document.querySelector('.slide-container');
|
346 |
+
|
347 |
+
// 最终强制设置
|
348 |
+
[html, body].forEach(element => {
|
349 |
+
if (element) {
|
350 |
+
element.style.width = targetWidth + 'px';
|
351 |
+
element.style.height = targetHeight + 'px';
|
352 |
+
element.style.minWidth = targetWidth + 'px';
|
353 |
+
element.style.minHeight = targetHeight + 'px';
|
354 |
+
element.style.maxWidth = targetWidth + 'px';
|
355 |
+
element.style.maxHeight = targetHeight + 'px';
|
356 |
+
element.style.margin = '0';
|
357 |
+
element.style.padding = '0';
|
358 |
+
element.style.border = 'none';
|
359 |
+
element.style.outline = 'none';
|
360 |
+
element.style.overflow = 'hidden';
|
361 |
+
element.style.position = 'fixed';
|
362 |
+
element.style.top = '0';
|
363 |
+
element.style.left = '0';
|
364 |
+
}
|
365 |
+
});
|
366 |
+
|
367 |
+
if (container) {
|
368 |
+
container.style.width = targetWidth + 'px';
|
369 |
+
container.style.height = targetHeight + 'px';
|
370 |
+
container.style.minWidth = targetWidth + 'px';
|
371 |
+
container.style.minHeight = targetHeight + 'px';
|
372 |
+
container.style.maxWidth = targetWidth + 'px';
|
373 |
+
container.style.maxHeight = targetHeight + 'px';
|
374 |
+
container.style.position = 'fixed';
|
375 |
+
container.style.top = '0';
|
376 |
+
container.style.left = '0';
|
377 |
+
container.style.margin = '0';
|
378 |
+
container.style.padding = '0';
|
379 |
+
container.style.border = 'none';
|
380 |
+
container.style.outline = 'none';
|
381 |
+
container.style.overflow = 'hidden';
|
382 |
+
container.style.transform = 'none';
|
383 |
+
container.style.boxShadow = 'none';
|
384 |
+
}
|
385 |
+
|
386 |
+
console.log('最终页面尺寸确认:', {
|
387 |
+
html: html.offsetWidth + 'x' + html.offsetHeight,
|
388 |
+
body: body.offsetWidth + 'x' + body.offsetHeight,
|
389 |
+
container: container ? container.offsetWidth + 'x' + container.offsetHeight : 'none'
|
390 |
+
});
|
391 |
+
}, width, height);
|
392 |
+
|
393 |
+
// 再次等待以确保所有更改生效
|
394 |
+
await page.waitForTimeout(1000);
|
395 |
+
|
396 |
+
// 执行截图,使用精确的剪裁区域
|
397 |
+
const screenshot = await page.screenshot({
|
398 |
+
type: options.format || 'png',
|
399 |
+
quality: options.quality || 100,
|
400 |
+
clip: {
|
401 |
+
x: 0,
|
402 |
+
y: 0,
|
403 |
+
width: width,
|
404 |
+
height: height
|
405 |
+
},
|
406 |
+
omitBackground: false, // 包含背景
|
407 |
+
captureBeyondViewport: false, // 不截取视口外内容
|
408 |
+
});
|
409 |
+
|
410 |
+
console.log(`截图完成, 生成了 ${screenshot.length} 字节的图片数据`);
|
411 |
+
return screenshot;
|
412 |
+
|
413 |
+
} finally {
|
414 |
+
await page.close();
|
415 |
+
}
|
416 |
+
|
417 |
+
} catch (error) {
|
418 |
+
console.error('截图失败:', error);
|
419 |
+
throw new Error(`截图失败: ${error.message}`);
|
420 |
+
}
|
421 |
}
|
422 |
}
|
423 |
|