
本文介绍如何在 scala 的 http4s 服务中,利用 openpdf 动态生成 pdf 并通过 http 响应直接返回给浏览器(内嵌预览或下载),关键在于正确处理字节流、避免 base64 编码、设置标准 pdf 响应头。
本文介绍如何在 scala 的 http4s 服务中,利用 openpdf 动态生成 pdf 并通过 http 响应直接返回给浏览器(内嵌预览或下载),关键在于正确处理字节流、避免 base64 编码、设置标准 pdf 响应头。
在基于 http4s(如 Ember 服务器)的 Scala Web 服务中,若需响应 PDF 请求,核心原则是:HTTP 响应体必须为原始二进制字节(Array[Byte]),而非字符串或 Base64 编码文本。原代码中将 ByteArrayOutputStream 的内容经 Base64.getEncoder.encodeToString(...) 转为字符串,再传入 Ok(...),这会导致浏览器接收到非法 PDF 内容(实际是 Base64 文本),无法解析渲染。
✅ 正确做法是让 createPdfSingleBook 直接返回 Array[Byte],并仅使用 ByteArrayOutputStream —— 完全弃用 FileOutputStream(磁盘写入非必需,且影响性能与无状态性)。OpenPDF 的 PdfWriter 支持任意 OutputStream,因此可安全绑定到内存流。
以下是优化后的完整实现:
import com.lowagie.text.Document
import com.lowagie.text.pdf.PdfWriter
import org.http4s._
import org.http4s.headers.`Content-Type`
import org.http4s.MediaType
def createPdfSingleBook(bookF: BookF): Array[Byte] = {
val document = new Document()
val baos = new java.io.ByteArrayOutputStream()
val writer = PdfWriter.getInstance(document, baos)
document.open()
// ✅ 在此处添加 PDF 内容(如 Paragraph、Table 等)
// import com.lowagie.text.Paragraph
// document.add(new Paragraph(s"Book: ${bookF.title}"))
document.close()
writer.close() // 必须关闭以确保所有内容写入 baos
baos.toByteArray // 直接返回原始字节,不编码
}在 http4s 路由中,使用该字节数组构造响应:
case GET -> Root / "pdf" / IntVar(bookId) =>
getBookF(bookId) match {
case Some(bookF: BookF) =>
val pdfBytes = createPdfSingleBook(bookF)
Ok(pdfBytes)
.withContentType(`Content-Type`(MediaType.application.pdf))
.putHeaders(
Header.Raw(CIString("Content-Disposition"), "inline; filename=book.pdf"),
Header.Raw(CIString("Cache-Control"), "no-cache, no-store, must-revalidate"),
Header.Raw(CIString("Pragma"), "no-cache"),
Header.Raw(CIString("Expires"), "0")
)
case None => NotFound()
}? 关键说明与注意事项:
- Content-Disposition: inline 指示浏览器优先内嵌显示 PDF(现代浏览器默认行为);若需强制下载,改为 attachment; filename="book.pdf"。
- 不必手动设置 Content-Length:http4s 会自动从 Array[Byte] 推导并添加该头。
- 移除 FileOutputStream 可提升吞吐量与可伸缩性(无 I/O 瓶颈、无临时文件清理问题)。
- 确保 OpenPDF 版本兼容(推荐 com.github.librepdf:openpdf:1.3.30+),避免旧版 iText 许可风险。
- 若 PDF 内容较大(>5MB),建议改用 Stream[IO, Byte] + fs2 流式响应,防止内存溢出。
通过以上改造,服务即可高效、标准地交付 PDF,用户点击链接后,浏览器将自动打开 PDF 预览(或触发下载),体验与静态 PDF 文件完全一致。


















