|
import puppeteer from 'puppeteer';
|
|
import playwright from 'playwright';
|
|
|
|
class ScreenshotService {
|
|
constructor() {
|
|
this.puppeteerBrowser = null;
|
|
this.playwrightBrowser = null;
|
|
this.isInitializing = false;
|
|
this.puppeteerReady = false;
|
|
this.playwrightReady = false;
|
|
|
|
console.log('Screenshot service initialized');
|
|
}
|
|
|
|
|
|
async initPuppeteer() {
|
|
if (this.puppeteerBrowser || this.isInitializing) return;
|
|
|
|
this.isInitializing = true;
|
|
|
|
try {
|
|
console.log('π Starting Puppeteer browser...');
|
|
|
|
const launchOptions = {
|
|
headless: 'new',
|
|
args: [
|
|
'--no-sandbox',
|
|
'--disable-setuid-sandbox',
|
|
'--disable-dev-shm-usage',
|
|
'--disable-gpu',
|
|
'--disable-extensions',
|
|
'--disable-background-timer-throttling',
|
|
'--disable-backgrounding-occluded-windows',
|
|
'--disable-renderer-backgrounding',
|
|
'--no-first-run',
|
|
'--no-default-browser-check',
|
|
'--disable-default-apps',
|
|
'--disable-features=TranslateUI',
|
|
'--disable-ipc-flooding-protection',
|
|
'--memory-pressure-off',
|
|
'--max_old_space_size=4096'
|
|
],
|
|
timeout: 30000
|
|
};
|
|
|
|
|
|
if (process.env.SPACE_ID) {
|
|
launchOptions.args.push(
|
|
'--single-process',
|
|
'--disable-background-networking',
|
|
'--disable-background-mode'
|
|
);
|
|
}
|
|
|
|
this.puppeteerBrowser = await puppeteer.launch(launchOptions);
|
|
this.puppeteerReady = true;
|
|
console.log('β
Puppeteer browser started successfully');
|
|
|
|
|
|
process.on('exit', () => this.cleanup());
|
|
process.on('SIGINT', () => this.cleanup());
|
|
process.on('SIGTERM', () => this.cleanup());
|
|
|
|
} catch (error) {
|
|
console.error('β Puppeteer initialization failed:', error.message);
|
|
this.puppeteerReady = false;
|
|
} finally {
|
|
this.isInitializing = false;
|
|
}
|
|
}
|
|
|
|
|
|
async initPlaywright() {
|
|
if (this.playwrightBrowser) return;
|
|
|
|
try {
|
|
console.log('π Starting Playwright browser...');
|
|
|
|
this.playwrightBrowser = await playwright.chromium.launch({
|
|
headless: true,
|
|
args: [
|
|
'--no-sandbox',
|
|
'--disable-setuid-sandbox',
|
|
'--disable-dev-shm-usage'
|
|
]
|
|
});
|
|
|
|
this.playwrightReady = true;
|
|
console.log('β
Playwright browser started successfully');
|
|
|
|
} catch (error) {
|
|
console.error('β Playwright initialization failed:', error.message);
|
|
this.playwrightReady = false;
|
|
}
|
|
}
|
|
|
|
|
|
async generateScreenshot(htmlContent, options = {}) {
|
|
const {
|
|
format = 'jpeg',
|
|
quality = 90,
|
|
width = 1000,
|
|
height = 562,
|
|
timeout = 15000
|
|
} = options;
|
|
|
|
console.log(`πΈ Generating screenshot: ${width}x${height}, ${format}@${quality}%`);
|
|
|
|
|
|
if (!this.puppeteerReady) {
|
|
await this.initPuppeteer();
|
|
}
|
|
|
|
if (this.puppeteerReady) {
|
|
try {
|
|
return await this.generateWithPuppeteer(htmlContent, { format, quality, width, height, timeout });
|
|
} catch (error) {
|
|
console.warn('Puppeteer screenshot failed, trying Playwright:', error.message);
|
|
}
|
|
}
|
|
|
|
|
|
if (!this.playwrightReady) {
|
|
await this.initPlaywright();
|
|
}
|
|
|
|
if (this.playwrightReady) {
|
|
try {
|
|
return await this.generateWithPlaywright(htmlContent, { format, quality, width, height, timeout });
|
|
} catch (error) {
|
|
console.warn('Playwright screenshot failed:', error.message);
|
|
}
|
|
}
|
|
|
|
|
|
console.warn('All screenshot methods failed, generating fallback SVG');
|
|
return this.generateFallbackImage(width, height, 'Screenshot Service', 'Browser unavailable');
|
|
}
|
|
|
|
|
|
async generateWithPuppeteer(htmlContent, options) {
|
|
const { format, quality, width, height, timeout } = options;
|
|
let page = null;
|
|
|
|
try {
|
|
page = await this.puppeteerBrowser.newPage();
|
|
|
|
|
|
await page.setViewport({ width, height });
|
|
|
|
|
|
await page.setContent(htmlContent, {
|
|
waitUntil: 'networkidle0',
|
|
timeout: timeout
|
|
});
|
|
|
|
|
|
await page.evaluate(() => {
|
|
return document.fonts ? document.fonts.ready : Promise.resolve();
|
|
});
|
|
|
|
|
|
await page.waitForTimeout(300);
|
|
|
|
|
|
const screenshotOptions = {
|
|
type: format,
|
|
quality: format === 'jpeg' ? quality : undefined,
|
|
fullPage: false,
|
|
clip: { x: 0, y: 0, width, height }
|
|
};
|
|
|
|
const screenshot = await page.screenshot(screenshotOptions);
|
|
|
|
console.log(`β
Puppeteer screenshot generated: ${screenshot.length} bytes`);
|
|
return screenshot;
|
|
|
|
} finally {
|
|
if (page) {
|
|
await page.close().catch(console.error);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
async generateWithPlaywright(htmlContent, options) {
|
|
const { format, quality, width, height, timeout } = options;
|
|
let page = null;
|
|
|
|
try {
|
|
page = await this.playwrightBrowser.newPage();
|
|
|
|
|
|
await page.setViewportSize({ width, height });
|
|
|
|
|
|
await page.setContent(htmlContent, {
|
|
waitUntil: 'networkidle',
|
|
timeout: timeout
|
|
});
|
|
|
|
|
|
await page.evaluate(() => {
|
|
return document.fonts ? document.fonts.ready : Promise.resolve();
|
|
});
|
|
|
|
|
|
const screenshotOptions = {
|
|
type: format,
|
|
quality: format === 'jpeg' ? quality : undefined,
|
|
fullPage: false,
|
|
clip: { x: 0, y: 0, width, height }
|
|
};
|
|
|
|
const screenshot = await page.screenshot(screenshotOptions);
|
|
|
|
console.log(`β
Playwright screenshot generated: ${screenshot.length} bytes`);
|
|
return screenshot;
|
|
|
|
} finally {
|
|
if (page) {
|
|
await page.close().catch(console.error);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
generateFallbackImage(width = 1000, height = 562, title = 'PPT Screenshot', subtitle = '', message = '') {
|
|
const svg = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
<defs>
|
|
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" style="stop-color:#f8f9fa;stop-opacity:1" />
|
|
<stop offset="100%" style="stop-color:#e9ecef;stop-opacity:1" />
|
|
</linearGradient>
|
|
<pattern id="dots" patternUnits="userSpaceOnUse" width="20" height="20">
|
|
<circle cx="2" cy="2" r="1" fill="#dee2e6" opacity="0.5"/>
|
|
</pattern>
|
|
</defs>
|
|
|
|
<!-- Background -->
|
|
<rect width="100%" height="100%" fill="url(#bg)"/>
|
|
<rect width="100%" height="100%" fill="url(#dots)"/>
|
|
|
|
<!-- Border -->
|
|
<rect x="20" y="20" width="${width-40}" height="${height-40}"
|
|
fill="none" stroke="#dee2e6" stroke-width="3"
|
|
stroke-dasharray="15,10" rx="10"/>
|
|
|
|
<!-- Icon -->
|
|
<g transform="translate(${width/2}, ${height*0.25})">
|
|
<circle cx="0" cy="0" r="30" fill="#6c757d" opacity="0.3"/>
|
|
<rect x="-15" y="-10" width="30" height="20" rx="3" fill="#6c757d"/>
|
|
<rect x="-12" y="-7" width="24" height="3" fill="white"/>
|
|
<rect x="-12" y="-2" width="24" height="3" fill="white"/>
|
|
<rect x="-12" y="3" width="16" height="3" fill="white"/>
|
|
</g>
|
|
|
|
<!-- Title -->
|
|
<text x="${width/2}" y="${height*0.45}"
|
|
text-anchor="middle"
|
|
font-family="Microsoft YaHei, PingFang SC, Hiragino Sans GB, Source Han Sans SC, Noto Sans SC, WenQuanYi Micro Hei, SimHei, SimSun, Arial, sans-serif"
|
|
font-size="28"
|
|
font-weight="bold"
|
|
fill="#495057">${title}</text>
|
|
|
|
${subtitle ? `
|
|
<!-- Subtitle -->
|
|
<text x="${width/2}" y="${height*0.55}"
|
|
text-anchor="middle"
|
|
font-family="Microsoft YaHei, PingFang SC, Hiragino Sans GB, Source Han Sans SC, Noto Sans SC, WenQuanYi Micro Hei, SimHei, SimSun, Arial, sans-serif"
|
|
font-size="18"
|
|
fill="#6c757d">${subtitle}</text>
|
|
` : ''}
|
|
|
|
${message ? `
|
|
<!-- Message -->
|
|
<text x="${width/2}" y="${height*0.65}"
|
|
text-anchor="middle"
|
|
font-family="Microsoft YaHei, PingFang SC, Hiragino Sans GB, Source Han Sans SC, Noto Sans SC, WenQuanYi Micro Hei, SimHei, SimSun, Arial, sans-serif"
|
|
font-size="14"
|
|
fill="#adb5bd">${message}</text>
|
|
` : ''}
|
|
|
|
<!-- Footer -->
|
|
<text x="${width/2}" y="${height*0.85}"
|
|
text-anchor="middle"
|
|
font-family="Microsoft YaHei, PingFang SC, Hiragino Sans GB, Source Han Sans SC, Noto Sans SC, WenQuanYi Micro Hei, SimHei, SimSun, Arial, sans-serif"
|
|
font-size="12"
|
|
fill="#ced4da">PPT Screenshot Service β’ ${new Date().toLocaleString('zh-CN')}</text>
|
|
|
|
<!-- Dimensions info -->
|
|
<text x="${width/2}" y="${height*0.92}"
|
|
text-anchor="middle"
|
|
font-family="Microsoft YaHei, PingFang SC, Hiragino Sans GB, Source Han Sans SC, Noto Sans SC, WenQuanYi Micro Hei, SimHei, SimSun, Arial, sans-serif"
|
|
font-size="10"
|
|
fill="#ced4da">Size: ${width} Γ ${height}</text>
|
|
</svg>`;
|
|
|
|
return Buffer.from(svg, 'utf-8');
|
|
}
|
|
|
|
|
|
getStatus() {
|
|
return {
|
|
puppeteerReady: this.puppeteerReady,
|
|
playwrightReady: this.playwrightReady,
|
|
environment: process.env.NODE_ENV || 'development',
|
|
isHuggingFace: !!process.env.SPACE_ID
|
|
};
|
|
}
|
|
|
|
|
|
async cleanup() {
|
|
console.log('π§Ή Cleaning up screenshot service...');
|
|
|
|
if (this.puppeteerBrowser) {
|
|
try {
|
|
await this.puppeteerBrowser.close();
|
|
this.puppeteerBrowser = null;
|
|
this.puppeteerReady = false;
|
|
console.log('β
Puppeteer browser closed');
|
|
} catch (error) {
|
|
console.error('Error closing Puppeteer browser:', error);
|
|
}
|
|
}
|
|
|
|
if (this.playwrightBrowser) {
|
|
try {
|
|
await this.playwrightBrowser.close();
|
|
this.playwrightBrowser = null;
|
|
this.playwrightReady = false;
|
|
console.log('β
Playwright browser closed');
|
|
} catch (error) {
|
|
console.error('Error closing Playwright browser:', error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export default new ScreenshotService(); |