438 lines
14 KiB
JavaScript
438 lines
14 KiB
JavaScript
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内容..."><!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>示例文档</title>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; margin: 40px; }
|
||
h1 { color: #333; }
|
||
.content { line-height: 1.6; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>Hello PDF!</h1>
|
||
<div class="content">
|
||
<p>这是一个通过HTML内容生成的PDF文档。</p>
|
||
<p>您可以在这里输入任何HTML内容。</p>
|
||
</div>
|
||
</body>
|
||
</html></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": "<h1>Hello PDF</h1>",
|
||
"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; |