Files
html2pdf-server/server.js
2025-10-20 15:18:52 +08:00

438 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const express = require('express');
const { chromium } = require('playwright');
const cors = require('cors');
const bodyParser = require('body-parser');
const path = require('path');
const fs = require('fs');
const app = express();
const PORT = process.env.PORT || 3000;
// 中间件
app.use(cors());
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' }));
// 确保输出目录存在
const outputDir = path.join(__dirname, 'output');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
class PdfService {
constructor() {
this.browser = null;
}
async initialize() {
if (this.browser) return;
this.browser = await chromium.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--disable-web-security'
]
});
console.log('Playwright浏览器初始化成功');
}
async generatePdf(options) {
if (!this.browser) {
await this.initialize();
}
const context = await this.browser.newContext();
const page = await context.newPage();
try {
const {
html,
url,
landscape = false,
format = 'A4',
margin = { top: 20, right: 20, bottom: 20, left: 20 },
printBackground = true,
timeout = 60000
} = options;
page.setDefaultTimeout(timeout);
if (url) {
console.log(`正在生成PDF: ${url}`);
await page.goto(url, {
waitUntil: 'networkidle',
timeout: timeout
});
} else if (html) {
console.log('正在转换HTML到PDF');
await page.setContent(html, {
waitUntil: 'networkidle',
timeout: timeout
});
} else {
throw new Error('必须提供html或url参数');
}
// 生成PDF
const pdfBuffer = await page.pdf({
landscape,
format,
margin,
printBackground,
preferCSSPageSize: false
});
return pdfBuffer;
} finally {
await context.close();
}
}
async close() {
if (this.browser) {
await this.browser.close();
}
}
}
const pdfService = new PdfService();
// 健康检查端点
app.get('/health', async (req, res) => {
try {
await pdfService.initialize();
res.json({
status: 'healthy',
service: 'HTML to PDF Service',
timestamp: new Date().toISOString()
});
} catch (error) {
res.status(503).json({
status: 'unhealthy',
error: error.message
});
}
});
// 通过URL生成PDF
app.get('/generate', async (req, res) => {
try {
const { url, download = 'false' } = req.query;
if (!url) {
return res.status(400).send(`
<html>
<body style="font-family: Arial, sans-serif; margin: 40px;">
<h1>PDF生成服务</h1>
<p>请提供URL参数例如</p>
<code>/generate?url=https://example.com</code>
</body>
</html>
`);
}
const pdfBuffer = await pdfService.generatePdf({
url: url,
timeout: 60000
});
const filename = `document-${Date.now()}.pdf`;
res.setHeader('Content-Type', 'application/pdf');
if (download === 'true') {
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
} else {
res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
}
res.send(pdfBuffer);
} catch (error) {
console.error('PDF生成错误:', error);
res.status(500).send(`
<html>
<body style="font-family: Arial, sans-serif; margin: 40px;">
<h1>PDF生成失败</h1>
<p>错误信息: ${error.message}</p>
<a href="/">返回首页</a>
</body>
</html>
`);
}
});
// 通过HTML内容生成PDF - POST接口
app.post('/generate/html', async (req, res) => {
try {
const { html, download = false, filename = `document-${Date.now()}.pdf` } = req.body;
if (!html) {
return res.status(400).json({
success: false,
error: 'HTML内容不能为空'
});
}
console.log('正在通过HTML内容生成PDF');
const pdfBuffer = await pdfService.generatePdf({
html: html,
timeout: 60000
});
res.setHeader('Content-Type', 'application/pdf');
if (download) {
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
} else {
res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
}
res.send(pdfBuffer);
} catch (error) {
console.error('HTML内容转PDF错误:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
// 通过HTML内容生成PDF - GET接口表单提交
app.get('/generate/html-form', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>HTML转PDF</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.container { max-width: 800px; margin: 0 auto; }
textarea {
width: 100%;
height: 300px;
padding: 10px;
margin: 10px 0;
border: 1px solid #ddd;
border-radius: 4px;
font-family: monospace;
}
input[type="text"] {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 10px 20px;
background: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
.button-group { margin: 20px 0; }
</style>
</head>
<body>
<div class="container">
<h1>HTML内容转PDF</h1>
<form id="htmlForm">
<label for="filename">文件名:</label>
<input type="text" id="filename" name="filename" value="document.pdf" placeholder="输入文件名">
<label for="htmlContent">HTML内容:</label>
<textarea id="htmlContent" name="html" placeholder="请输入HTML内容...">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;示例文档&lt;/title&gt;
&lt;style&gt;
body { font-family: Arial, sans-serif; margin: 40px; }
h1 { color: #333; }
.content { line-height: 1.6; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Hello PDF!&lt;/h1&gt;
&lt;div class="content"&gt;
&lt;p&gt;这是一个通过HTML内容生成的PDF文档。&lt;/p&gt;
&lt;p&gt;您可以在这里输入任何HTML内容。&lt;/p&gt;
&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;</textarea>
<div class="button-group">
<button type="button" onclick="generatePDF(false)">在线查看PDF</button>
<button type="button" onclick="generatePDF(true)">下载PDF</button>
</div>
</form>
<a href="/" style="color: #007bff;">返回首页</a>
</div>
<script>
function generatePDF(download) {
const form = document.getElementById('htmlForm');
const formData = new FormData(form);
const data = {
html: document.getElementById('htmlContent').value,
download: download,
filename: document.getElementById('filename').value
};
fetch('/generate/html', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
})
.then(response => {
if (!response.ok) {
return response.json().then(err => { throw new Error(err.error); });
}
return response.blob();
})
.then(blob => {
if (download) {
// 下载文件
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = data.filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} else {
// 在新窗口打开
const url = window.URL.createObjectURL(blob);
window.open(url, '_blank');
window.URL.revokeObjectURL(url);
}
})
.catch(error => {
alert('生成PDF失败: ' + error.message);
});
}
</script>
</body>
</html>
`);
});
// 首页
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>PDF生成服务</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.container { max-width: 800px; margin: 0 auto; }
.button {
display: inline-block;
padding: 10px 20px;
margin: 10px;
background: #007bff;
color: white;
text-decoration: none;
border-radius: 5px;
}
.button:hover { background: #0056b3; }
.button-green {
background: #28a745;
}
.button-green:hover {
background: #218838;
}
form { margin: 20px 0; }
input[type="text"] {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ddd;
border-radius: 4px;
}
.feature-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>PDF生成服务</h1>
<div class="feature-card">
<h2>🔗 URL转PDF</h2>
<p>通过网页URL生成PDF文档</p>
<form action="/generate" method="GET">
<label for="url">输入URL:</label>
<input type="text" id="url" name="url" placeholder="https://example.com">
<br>
<label>
<input type="checkbox" name="download" value="true"> 下载
</label>
<br><br>
<button type="submit" style="padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">生成PDF</button>
</form>
</div>
<div class="feature-card">
<h2>📝 HTML内容转PDF</h2>
<p>通过HTML代码直接生成PDF文档</p>
<a class="button button-green" href="/generate/html-form">使用HTML转PDF工具</a>
</div>
<div style="margin-top: 30px; padding: 20px; background: #f8f9fa; border-radius: 8px;">
<h3>API接口说明</h3>
<p><strong>URL转PDF:</strong> GET /generate?url=https://example.com</p>
<p><strong>HTML转PDF:</strong> POST /generate/html</p>
<pre>
// 请求示例
{
"html": "&lt;h1&gt;Hello PDF&lt;/h1&gt;",
"download": true,
"filename": "document.pdf"
}
</pre>
</div>
</div>
</body>
</html>
`);
});
// 优雅关闭
process.on('SIGINT', async () => {
console.log('正在关闭服务...');
await pdfService.close();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.log('收到终止信号,正在关闭服务...');
await pdfService.close();
process.exit(0);
});
// 启动服务
app.listen(PORT, async () => {
console.log(`HTML转PDF服务运行在端口 ${PORT}`);
console.log(`访问地址: http://localhost:${PORT}`);
console.log(`URL转PDF: http://localhost:${PORT}/generate?url=https://example.com`);
console.log(`HTML转PDF表单: http://localhost:${PORT}/generate/html-form`);
});
module.exports = app;