Skip to content

PDF Oxide をはじめよう(Scala)

PDF Oxide は、テキスト抽出を標準搭載した JVM 向け最速の PDF ライブラリです。3,830 件の PDF で平均 0.8ms、合格率 100% を記録しています。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 ファサードは、唯一の JNI ネイティブブリッジを担う fyi.oxide:pdf-oxide Java バインディングに依存します。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.fromMarkdownPdf ハンドルを返します。pdf.save() はそれを Array[Byte] にシリアライズします。PdfDocument.open はそのバイト列を受け取り、ドキュメント API を公開します。

テキスト抽出

プレーンテキスト

任意のページから、0 始まりのインデックスを指定してプレーンテキストを抽出します。

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

ドキュメント全体を 1 回の呼び出しで 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 の Seq として公開します。wordsSeqlinesSeqcharsSeqtablesSeqimagesSeqannotationsSeq です。各 TextWordtextbbox を保持します。

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 として反復処理することもできます(その size は 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 になりうるドキュメントのメタデータは、producerOptioncreatorOption を通じて Option[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、オプションの markdownhtml レンダリング、そして OCR がまだ必要なページのリストが含まれ、いずれもファサードを通じてイディオマティックに公開されます(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)

次のステップ