You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Ubuntu环境下Swift/Vapor生成PDF并返回前端的最优方案咨询

我之前在Vapor项目里折腾过Ubuntu环境下的PDF生成需求,结合不同场景给你推荐几个靠谱的方案,你可以根据自己的需求选:

方案1:原生Swift库直接生成PDF(适合简单场景)

如果你的PDF内容结构不复杂(比如简单文本、基础表格),用纯Swift库是最省心的,不需要依赖外部工具,完全集成在Vapor项目里。推荐用SwiftPdf,它是跨平台的,完美支持Linux。

步骤:

  1. 先在Package.swift里添加依赖:
dependencies: [
    .package(url: "https://github.com/OpenSwiftUI/SwiftPdf.git", from: "0.1.0"),
    // 别忘了你的Vapor依赖
],
targets: [
    .executableTarget(
        name: "App",
        dependencies: [
            .product(name: "Vapor", package: "vapor"),
            .product(name: "SwiftPdf", package: "SwiftPdf")
        ]
    ),
    // 其他target...
]
  1. 在路由里实现PDF生成和返回:
import Vapor
import SwiftPdf

func routes(_ app: Application) throws {
    app.get("generate-simple-pdf") { req async throws -> Response in
        // 构建PDF内容
        let document = PDFDocument {
            PDFPage {
                PDFText("Vapor PDF 生成示例", font: .boldSystemFont(ofSize: 24))
                    .frame(maxWidth: .infinity, alignment: .center)
                    .padding(.bottom, 20)
                
                PDFTable(columns: 3) {
                    PDFTableRow {
                        PDFTableCell(PDFText("ID"))
                        PDFTableCell(PDFText("商品名称"))
                        PDFTableCell(PDFText("价格"))
                    }
                    PDFTableRow {
                        PDFTableCell(PDFText("1"))
                        PDFTableCell(PDFText("笔记本电脑"))
                        PDFTableCell(PDFText("¥5999"))
                    }
                    PDFTableRow {
                        PDFTableCell(PDFText("2"))
                        PDFTableCell(PDFText("无线鼠标"))
                        PDFTableCell(PDFText("¥99"))
                    }
                }
                .border(width: 1)
            }
        }
        
        // 渲染PDF为Data
        let pdfData = try document.render()
        
        // 构造响应,设置正确的Content-Type和下载文件名
        let response = Response()
        response.headers.contentType = .pdf
        response.headers.add(name: .contentDisposition, value: "attachment; filename=\"simple-report.pdf\"")
        response.body = .init(data: pdfData)
        
        return response
    }
}

优点:纯Swift代码,无外部依赖,部署简单;缺点:复杂布局(比如多列、自定义样式)的灵活性不如HTML。

方案2:HTML转PDF(适合复杂布局,首推)

如果需要生成带复杂样式、多元素的PDF(比如报表、合同),HTML/CSS的布局能力比原生PDF库强太多。这里推荐用wkhtmltopdf这个命令行工具,Ubuntu上可以直接安装,然后在Vapor里调用外部进程生成PDF。

步骤:

  1. 在Ubuntu服务器上安装wkhtmltopdf:
sudo apt update
sudo apt install wkhtmltopdf
  1. 在Vapor中实现HTML渲染和PDF生成:
    你可以直接拼接HTML字符串,或者用Vapor的Leaf模板引擎渲染更复杂的页面(推荐用Leaf,维护起来更方便)。
import Vapor

func routes(_ app: Application) throws {
    app.get("generate-complex-pdf") { req async throws -> Response in
        // 用Leaf渲染HTML(假设你已经配置了Leaf)
        let context = [
            "title": "复杂报表",
            "items": [
                ["id": "1", "name": "台式机", "price": "¥8999"],
                ["id": "2", "name": "显示器", "price": "¥1299"]
            ]
        ]
        let htmlContent = try req.view.render("report", context).renderAsString()
        
        // 创建临时文件存HTML
        let tempHtmlPath = req.application.tempDirectory.appending(path: "temp_\(UUID().uuidString).html")
        try htmlContent.write(to: tempHtmlPath, atomically: true, encoding: .utf8)
        
        // 调用wkhtmltopdf生成PDF
        let tempPdfPath = req.application.tempDirectory.appending(path: "temp_\(UUID().uuidString).pdf")
        let process = Process()
        process.executableURL = URL(fileURLWithPath: "/usr/bin/wkhtmltopdf")
        process.arguments = [tempHtmlPath.path, tempPdfPath.path]
        
        try process.run()
        process.waitUntilExit()
        
        // 检查生成是否成功
        guard process.terminationStatus == 0 else {
            throw Abort(.internalServerError, reason: "PDF生成失败")
        }
        
        // 读取PDF数据
        let pdfData = try Data(contentsOf: tempPdfPath)
        
        // 清理临时文件(可选,也可以让系统自动清理)
        try? FileManager.default.removeItem(at: tempHtmlPath)
        try? FileManager.default.removeItem(at: tempPdfPath)
        
        // 返回响应
        let response = Response()
        response.headers.contentType = .pdf
        response.headers.add(name: .contentDisposition, value: "attachment; filename=\"complex-report.pdf\"")
        response.body = .init(data: pdfData)
        
        return response
    }
}

优点:布局灵活,熟悉HTML/CSS就能快速实现复杂PDF;缺点:依赖外部命令行工具,需要确保服务器上安装了wkhtmltopdf。

方案3:Gotenberg服务(适合高并发/多场景需求)

如果你的服务有高并发需求,或者需要支持多种PDF生成方式(比如Office转PDF、URL转PDF),可以用Gotenberg这个专门的PDF生成服务。它基于Chromium,用Docker部署很方便,Vapor只需要调用它的API即可。

步骤:

  1. 用Docker部署Gotenberg:
docker run --rm -p 3000:3000 gotenberg/gotenberg:7

(生产环境可以配置持久化、负载均衡等)

  1. 在Vapor中调用Gotenberg的API:
import Vapor

func routes(_ app: Application) throws {
    app.get("generate-pdf-gotenberg") { req async throws -> Response in
        // 生成HTML内容(或用Leaf渲染)
        let htmlContent = """
        <!DOCTYPE html>
        <html>
        <head>
            <style>
                body { font-family: "Microsoft YaHei", sans-serif; }
                .header { text-align: center; padding: 20px; border-bottom: 1px solid #ddd; }
                table { width: 100%; border-collapse: collapse; margin-top: 20px; }
                th { background-color: #f5f5f5; }
                th, td { border: 1px solid #ddd; padding: 10px; }
            </style>
        </head>
        <body>
            <div class="header">
                <h1>Gotenberg生成的PDF</h1>
            </div>
            <table>
                <tr><th>ID</th><th>产品</th><th>价格</th></tr>
                <tr><td>1</td><td>耳机</td><td>¥399</td></tr>
                <tr><td>2</td><td>键盘</td><td>¥299</td></tr>
            </table>
        </body>
        </html>
        """
        
        // 构造表单数据发送给Gotenberg
        let formPart = FormDataPart(
            name: "file",
            filename: "index.html",
            contentType: .html,
            data: Data(htmlContent.utf8)
        )
        let formData = try FormDataEncoder().encode([formPart])
        
        // 调用Gotenberg API
        let gotenbergResponse = try await req.client.post(URI(string: "http://localhost:3000/convert/html")) { request in
            request.headers.contentType = .multipartFormData
            request.body = .init(data: formData)
        }
        
        guard gotenbergResponse.status == .ok else {
            throw Abort(.internalServerError, reason: "Gotenberg生成PDF失败")
        }
        
        // 返回PDF给客户端
        let response = Response()
        response.headers.contentType = .pdf
        response.headers.add(name: .contentDisposition, value: "attachment; filename=\"gotenberg-report.pdf\"")
        response.body = gotenbergResponse.body
        
        return response
    }
}

优点:解耦PDF生成逻辑,支持高并发和多种生成方式,可横向扩展;缺点:需要额外部署和维护Gotenberg服务。


内容的提问来源于stack exchange,提问作者Philip Pegden

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 07:26:14