Skip to content

Початок роботи з PDF Oxide (Scala)

PDF Oxide — це найшвидша бібліотека для роботи з PDF на JVM із вбудованим видобуванням тексту: середній час 0.8 мс і 100% успішних розборів на 3830 PDF. Біндинг для Scala 3 — це тонкий ідіоматичний фасад поверх зрілого Java-біндингу: він не додає жодного нативного коду, а лише накладає Scala-розширення (extension methods), що перетворюють 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)

Наступні кроки