Начало работы с PDF Oxide (Scala)
PDF Oxide — самая быстрая PDF-библиотека для JVM со встроенным извлечением текста: среднее время 0.8 мс, 100% успешных результатов на 3830 PDF. Привязка для Scala 3 — это тонкий идиоматичный фасад поверх зрелой Java-привязки: она не добавляет нативного кода, а лишь накладывает методы-расширения 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 зависит от Java-привязки fyi.oxide:pdf-oxide, которой принадлежит единственный нативный мост JNI. Требуется Scala 3.3+.
Быстрый старт
Соберите PDF из Markdown, затем откройте его и извлеките текст обратно. 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. Фасад предоставляет каждый извлекатель элементов в виде Scala-Seq через методы-расширения *Seq: wordsSeq, linesSeq, charsSeq, tablesSeq, imagesSeq и annotationsSeq. Каждое 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}")
Также можно обойти все страницы как Seq с помощью doc.pagesSeq (его размер совпадает с 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
Метаданные документа, допускающие null, представлены как Option[String] через producerOption и creatorOption, поэтому отсутствующие значения вы обрабатываете в духе 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 — всё это предоставляется идиоматично через фасад (markdownOption, htmlOption, pagesNeedingOcrSeq).
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)
Дальнейшие шаги
- Начало работы с Rust – использование PDF Oxide из Rust
- Начало работы с Python – использование PDF Oxide из Python
- Извлечение текста – подробные параметры и рецепты извлечения
- Создание PDF – продвинутое создание, шифрование и метаданные
- Редактирование – изменение существующих PDF, аннотации и поля форм