n8n / pdf-server.js
Perspicacious's picture
Update pdf-server.js
ca6518a verified
const express = require('express');
const { chromium } = require('playwright-core');
const fs = require('fs');
const path = require('path');
const app = express();
app.use(express.json({ limit: '50mb' }));
const PORT = 3000;
const CHROMIUM_PATH = '/usr/bin/chromium-browser';
// Arguments Chromium pour environnement restreint (HuggingFace)
const CHROMIUM_ARGS = [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--disable-software-rasterizer',
'--disable-extensions',
'--disable-background-networking',
'--disable-sync',
'--disable-translate',
'--disable-crash-reporter',
'--disable-breakpad',
'--disable-features=TranslateUI',
'--disable-ipc-flooding-protection',
'--no-first-run',
'--no-zygote',
'--single-process',
'--deterministic-fetch',
'--disable-features=IsolateOrigins',
'--disable-site-isolation-trials',
];
// Devices
const DEVICES = {
'desktop': { width: 1920, height: 1080 },
'laptop': { width: 1366, height: 768 },
'tablet': { width: 768, height: 1024 },
'mobile': { width: 375, height: 812 },
};
// Formats PDF
const FORMATS = {
'6x9': { width: '6in', height: '9in' },
'5x8': { width: '5in', height: '8in' },
'5.5x8.5': { width: '5.5in', height: '8.5in' },
'8.5x11': { width: '8.5in', height: '11in' },
'A4': { width: '210mm', height: '297mm' },
'A5': { width: '148mm', height: '210mm' },
};
// ==================== HEALTH ====================
app.get('/health', async (req, res) => {
let browser = null;
try {
browser = await chromium.launch({
executablePath: CHROMIUM_PATH,
headless: true,
args: CHROMIUM_ARGS,
});
await browser.close();
res.json({ status: 'ok', chromium: 'working' });
} catch (error) {
res.json({ status: 'error', chromium: error.message });
}
});
// ==================== PDF ====================
app.post('/generate-pdf', async (req, res) => {
let browser = null;
const startTime = Date.now();
try {
const {
html,
url,
format = '6x9',
margins = { top: '0.75in', bottom: '0.75in', left: '0.5in', right: '0.5in' },
printBackground = true,
} = req.body;
console.log(`[PDF] Starting... format=${format}`);
if (!html && !url) {
return res.status(400).json({ error: 'html ou url requis' });
}
console.log('[PDF] Launching browser...');
browser = await chromium.launch({
executablePath: CHROMIUM_PATH,
headless: true,
args: CHROMIUM_ARGS,
});
console.log(`[PDF] Browser launched (${Date.now() - startTime}ms)`);
const page = await browser.newPage();
console.log(`[PDF] Page created (${Date.now() - startTime}ms)`);
if (url) {
console.log(`[PDF] Loading URL: ${url}`);
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
} else {
console.log(`[PDF] Setting HTML content (${html.length} chars)`);
await page.setContent(html, { waitUntil: 'domcontentloaded', timeout: 30000 });
}
console.log(`[PDF] Content loaded (${Date.now() - startTime}ms)`);
// Attendre un peu pour le rendu
await page.waitForTimeout(1000);
const formatConfig = FORMATS[format] || FORMATS['6x9'];
console.log(`[PDF] Generating with format: ${JSON.stringify(formatConfig)}`);
const pdfBuffer = await page.pdf({
width: formatConfig.width,
height: formatConfig.height,
margin: margins,
printBackground,
});
console.log(`[PDF] Generated! Size: ${pdfBuffer.length} bytes (${Date.now() - startTime}ms)`);
await browser.close();
browser = null;
res.json({
success: true,
pdf: pdfBuffer.toString('base64'),
size_bytes: pdfBuffer.length,
size_mb: (pdfBuffer.length / 1024 / 1024).toFixed(2),
duration_ms: Date.now() - startTime,
});
} catch (error) {
console.error(`[PDF] ERROR: ${error.message}`);
console.error(error.stack);
if (browser) {
try { await browser.close(); } catch (e) {}
}
res.status(500).json({
error: error.message,
stack: error.stack,
duration_ms: Date.now() - startTime,
});
}
});
// ==================== SCREENSHOT ====================
app.post('/screenshot', async (req, res) => {
let browser = null;
try {
const {
url,
html,
device = 'desktop',
fullPage = true,
width,
height,
selector,
delay = 0,
} = req.body;
if (!url && !html) {
return res.status(400).json({ error: 'url ou html requis' });
}
const deviceConfig = DEVICES[device] || DEVICES['desktop'];
const viewportWidth = width || deviceConfig.width;
const viewportHeight = height || deviceConfig.height;
browser = await chromium.launch({
executablePath: CHROMIUM_PATH,
headless: true,
args: CHROMIUM_ARGS,
});
const context = await browser.newContext({
viewport: { width: viewportWidth, height: viewportHeight },
isMobile: device.includes('mobile') || device === 'tablet',
hasTouch: device.includes('mobile') || device === 'tablet',
});
const page = await context.newPage();
if (url) {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
} else {
await page.setContent(html, { waitUntil: 'domcontentloaded' });
}
if (delay > 0) {
await page.waitForTimeout(delay);
}
let screenshot;
if (selector) {
const element = await page.$(selector);
if (element) {
screenshot = await element.screenshot({ type: 'png' });
} else {
screenshot = await page.screenshot({ type: 'png', fullPage });
}
} else {
screenshot = await page.screenshot({ type: 'png', fullPage });
}
await browser.close();
res.json({
success: true,
screenshot: screenshot.toString('base64'),
device,
viewport: { width: viewportWidth, height: viewportHeight },
});
} catch (error) {
console.error(`[SCREENSHOT] ERROR: ${error.message}`);
if (browser) {
try { await browser.close(); } catch (e) {}
}
res.status(500).json({ error: error.message });
}
});
// ==================== MULTI-SCREENSHOT ====================
app.post('/multi-screenshot', async (req, res) => {
let browser = null;
try {
const {
url,
devices = ['desktop', 'tablet', 'mobile'],
fullPage = true,
} = req.body;
if (!url) {
return res.status(400).json({ error: 'url requis' });
}
browser = await chromium.launch({
executablePath: CHROMIUM_PATH,
headless: true,
args: CHROMIUM_ARGS,
});
const screenshots = {};
for (const device of devices) {
const deviceConfig = DEVICES[device] || DEVICES['desktop'];
const context = await browser.newContext({
viewport: { width: deviceConfig.width, height: deviceConfig.height },
isMobile: device.includes('mobile') || device === 'tablet',
});
const page = await context.newPage();
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
const screenshot = await page.screenshot({ type: 'png', fullPage });
screenshots[device] = screenshot.toString('base64');
await context.close();
}
await browser.close();
res.json({ success: true, screenshots, devices });
} catch (error) {
console.error(`[MULTI-SCREENSHOT] ERROR: ${error.message}`);
if (browser) {
try { await browser.close(); } catch (e) {}
}
res.status(500).json({ error: error.message });
}
});
// ==================== SCRAPE ====================
app.post('/scrape', async (req, res) => {
let browser = null;
try {
const {
url,
device = 'desktop',
selectors = {},
waitFor,
javascript,
} = req.body;
if (!url) {
return res.status(400).json({ error: 'url requis' });
}
const deviceConfig = DEVICES[device] || DEVICES['desktop'];
browser = await chromium.launch({
executablePath: CHROMIUM_PATH,
headless: true,
args: CHROMIUM_ARGS,
});
const page = await browser.newPage();
await page.setViewportSize({ width: deviceConfig.width, height: deviceConfig.height });
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
if (waitFor) {
await page.waitForSelector(waitFor, { timeout: 10000 }).catch(() => {});
}
const result = {
url: page.url(),
title: await page.title(),
};
// Extraire avec les sélecteurs
for (const [key, selector] of Object.entries(selectors)) {
try {
result[key] = await page.$$eval(selector, els => els.map(el => ({
text: el.textContent?.trim(),
href: el.href || null,
src: el.src || null,
})));
} catch (e) {
result[key] = [];
}
}
// JavaScript custom
if (javascript) {
try {
result.custom = await page.evaluate(javascript);
} catch (e) {
result.custom = { error: e.message };
}
}
await browser.close();
res.json({ success: true, ...result });
} catch (error) {
console.error(`[SCRAPE] ERROR: ${error.message}`);
if (browser) {
try { await browser.close(); } catch (e) {}
}
res.status(500).json({ error: error.message });
}
});
// ==================== SCROLL & SCREENSHOT (Alternative à vidéo) ====================
app.post('/scroll-capture', async (req, res) => {
let browser = null;
try {
const {
url,
device = 'desktop',
steps = 5,
delayBetweenSteps = 500,
} = req.body;
if (!url) {
return res.status(400).json({ error: 'url requis' });
}
const deviceConfig = DEVICES[device] || DEVICES['desktop'];
browser = await chromium.launch({
executablePath: CHROMIUM_PATH,
headless: true,
args: CHROMIUM_ARGS,
});
const page = await browser.newPage();
await page.setViewportSize({ width: deviceConfig.width, height: deviceConfig.height });
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
const screenshots = [];
const scrollHeight = await page.evaluate(() => document.body.scrollHeight);
const stepHeight = scrollHeight / steps;
for (let i = 0; i <= steps; i++) {
await page.evaluate((y) => window.scrollTo(0, y), i * stepHeight);
await page.waitForTimeout(delayBetweenSteps);
const screenshot = await page.screenshot({ type: 'png' });
screenshots.push(screenshot.toString('base64'));
}
await browser.close();
res.json({
success: true,
screenshots,
count: screenshots.length,
device,
});
} catch (error) {
console.error(`[SCROLL-CAPTURE] ERROR: ${error.message}`);
if (browser) {
try { await browser.close(); } catch (e) {}
}
res.status(500).json({ error: error.message });
}
});
// ==================== START ====================
app.listen(PORT, '0.0.0.0', () => {
console.log(`🚀 PDF Server v2 running on port ${PORT}`);
console.log(`📄 Endpoints: /health, /generate-pdf, /screenshot, /multi-screenshot, /scrape, /scroll-capture`);
});