Skip to content

PDF Oxide 快速上手(Scala)

PDF Oxide 是 JVM 上最快的 PDF 库,内置文本提取功能——在 3,830 个 PDF 上平均 0.8ms、100% 通过率。Scala 3 绑定是在成熟的 Java 绑定之上做的一层轻量、符合 Scala 习惯的封装:它不引入任何原生代码,只是叠加了一组 Scala 扩展方法,把 java.util.Optional[T] 变成 Option[T]、把 java.util.List[T] 变成 Seq[T]AutoCloseable 句柄可以直接配合 scala.util.Using 使用。

安装

build.sbt 中添加依赖:

libraryDependencies += "fyi.oxide" % "pdf-oxide" % "0.3.69"

Scala 封装依赖于 fyi.oxide:pdf-oxide 这个 Java 绑定,后者负责管理唯一的 JNI 原生桥接。需要 Scala 3.3 及以上版本。

快速上手

先从 Markdown 构建一个 PDF,再打开它并把文本提取出来。Using.resource 会自动帮你关闭每个句柄。

import fyi.oxide.pdf.{Pdf, PdfDocument, producerOption}
import scala.util.Using

Using.resource(Pdf.fromMarkdown("# Hello pdf_oxide\n\nThis is a **Scala** binding.\n")): pdf =>
  Using.resource(PdfDocument.open(pdf.save())): doc =>
    println(s"pages:    ${doc.pageCount()}")
    println(s"producer: ${doc.producerOption.getOrElse("(none)")}")
    println(doc.extractText(0))

Pdf.fromMarkdown 返回一个 Pdf 句柄;pdf.save() 把它序列化为 Array[Byte]PdfDocument.open 接收这些字节并暴露文档 API。

文本提取

纯文本

通过从零开始的页码索引,从任意页面提取纯文本。

import fyi.oxide.pdf.PdfDocument
import scala.util.Using

Using.resource(PdfDocument.open(pdfBytes)): doc =>
  assert(doc.isOpen)
  val text = doc.extractText(0)
  println(text)

Markdown 和 HTML

只需一次调用即可把整个文档转换为 Markdown 或 HTML。

import fyi.oxide.pdf.PdfDocument
import scala.util.Using

Using.resource(PdfDocument.open(pdfBytes)): doc =>
  println(doc.toMarkdown())
  println(doc.toHtml())

页面元素

doc.page(i) 返回一个 PdfPage。封装通过 *Seq 扩展方法把每种元素提取器暴露为 Scala 的 SeqwordsSeqlinesSeqcharsSeqtablesSeqimagesSeqannotationsSeq。每个 TextWord 都携带自身的 text 和一个 bbox

import fyi.oxide.pdf.{PdfDocument, wordsSeq, linesSeq, charsSeq, tablesSeq, imagesSeq, annotationsSeq}
import scala.util.Using

Using.resource(PdfDocument.open(pdfBytes)): doc =>
  val page = doc.page(0)
  println(s"size: ${page.width()} x ${page.height()}")

  page.wordsSeq.take(8).foreach { w =>
    println(s"  ${w.text} @ ${w.bbox}  (w=${w.bbox.width})")
  }

  println(s"lines:       ${page.linesSeq.size}")
  println(s"chars:       ${page.charsSeq.size}")
  println(s"tables:      ${page.tablesSeq.size}")
  println(s"images:      ${page.imagesSeq.size}")
  println(s"annotations: ${page.annotationsSeq.size}")

你也可以用 doc.pagesSeq 把每一页当作 Seq 来遍历(它的大小与 doc.pageCount() 一致)。

import fyi.oxide.pdf.{PdfDocument, pagesSeq, wordsSeq}
import scala.util.Using

Using.resource(PdfDocument.open(pdfBytes)): doc =>
  doc.pagesSeq.zipWithIndex.foreach { (page, i) =>
    println(s"page $i: ${page.wordsSeq.size} words")
  }

搜索

doc.searchSeq(query) 返回一个 Seq[SearchMatch]。每个匹配项都暴露其 text

import fyi.oxide.pdf.{PdfDocument, searchSeq}
import scala.util.Using

Using.resource(PdfDocument.open(pdfBytes)): doc =>
  val matches = doc.searchSeq("Hello")
  println(s"${matches.size} match(es)")
  matches.foreach(m => println(s"  ${m.text}"))

以 Option 表示的元数据

可空的文档元数据通过 producerOptioncreatorOptionOption[String] 的形式呈现,让你能以 Scala 的方式处理缺失值。

import fyi.oxide.pdf.{PdfDocument, producerOption, creatorOption}
import scala.util.Using

Using.resource(PdfDocument.open(pdfBytes)): doc =>
  println(doc.producerOption.getOrElse("(unknown producer)"))
  println(doc.creatorOption.getOrElse("(unknown creator)"))

  // 表单字段同样以 Seq 返回:
  println(s"form fields: ${doc.formFieldsSeq.size}")

渲染

doc.render(i) 把一个页面光栅化,并返回编码后的图像字节。

import fyi.oxide.pdf.PdfDocument
import scala.util.Using

Using.resource(PdfDocument.open(pdfBytes)): doc =>
  val png = doc.render(0)
  java.nio.file.Files.write(java.nio.file.Path.of("page-0.png"), png)

自动提取

AutoExtractor.of(doc).extractDocument() 返回一个 AutoResult,其中包含提取出的 text、可选的 markdown/html 渲染结果,以及仍需 OCR 的页面列表——这一切都通过封装以符合 Scala 习惯的方式暴露出来(markdownOptionhtmlOptionpagesNeedingOcrSeq)。

import fyi.oxide.pdf.{PdfDocument, AutoExtractor, markdownOption, htmlOption, pagesNeedingOcrSeq}
import scala.util.Using

Using.resource(PdfDocument.open(pdfBytes)): doc =>
  val result = AutoExtractor.of(doc).extractDocument()
  println(result.text)
  result.markdownOption.foreach(println)
  result.htmlOption.foreach(println)
  println(s"pages needing OCR: ${result.pagesNeedingOcrSeq}")

编辑

DocumentEditor.open 打开一个已有的 PDF 以进行结构性编辑。这里我们清除元数据,并把结果重新序列化为字节。

import fyi.oxide.pdf.DocumentEditor
import scala.util.Using

Using.resource(DocumentEditor.open(pdfBytes)): editor =>
  assert(editor.isOpen)
  editor.scrubMetadata()
  val cleaned: Array[Byte] = editor.save()
  java.nio.file.Files.write(java.nio.file.Path.of("scrubbed.pdf"), cleaned)

后续步骤