init
This commit is contained in:
438
server.js
Normal file
438
server.js
Normal file
@@ -0,0 +1,438 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user