Skip to content

Primeiros passos com o PDF Oxide (Scala)

O PDF Oxide é a biblioteca de PDF mais rápida para a JVM com extração de texto embutida — 0,8ms de média e 100% de aprovação em 3.830 PDFs. O binding para Scala 3 é uma fachada fina e idiomática sobre o consolidado binding Java: não adiciona nenhum código nativo e acrescenta métodos de extensão Scala que transformam java.util.Optional[T] em Option[T] e java.util.List[T] em Seq[T]. Os handles AutoCloseable funcionam diretamente com scala.util.Using.

Instalação

Adicione a dependência ao seu build.sbt:

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

A fachada Scala depende do binding Java fyi.oxide:pdf-oxide, responsável pela única ponte nativa JNI. É necessário Scala 3.3 ou superior.

Guia Rápido

Crie um PDF a partir de Markdown, depois abra-o e extraia o texto de volta. O Using.resource fecha cada handle para você.

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 retorna um handle Pdf; pdf.save() o serializa em um Array[Byte]. PdfDocument.open aceita esses bytes e expõe a API do documento.

Extração de Texto

Texto Simples

Extraia texto simples de qualquer página pelo seu índice baseado em zero.

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 e HTML

Converta o documento inteiro para Markdown ou HTML em uma única chamada.

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

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

Elementos da Página

doc.page(i) retorna um PdfPage. A fachada expõe cada extrator de elementos como um Seq Scala por meio dos métodos de extensão *Seq: wordsSeq, linesSeq, charsSeq, tablesSeq, imagesSeq e annotationsSeq. Cada TextWord carrega seu text e um 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}")

Você também pode percorrer todas as páginas como um Seq com doc.pagesSeq (cujo tamanho corresponde a 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")
  }

Busca

doc.searchSeq(query) retorna um Seq[SearchMatch]. Cada correspondência expõe seu 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}"))

Metadados como Option

Metadados anuláveis do documento aparecem como Option[String] por meio de producerOption e creatorOption, para que você trate os valores ausentes do jeito 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)"))

  // Os campos de formulário também voltam como um Seq:
  println(s"form fields: ${doc.formFieldsSeq.size}")

Renderização

doc.render(i) rasteriza uma página e retorna os bytes da imagem codificada.

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)

Extração Automática

AutoExtractor.of(doc).extractDocument() retorna um AutoResult com o text extraído, renderizações opcionais em markdown/html e a lista de páginas que ainda precisam de OCR — tudo exposto de forma idiomática pela fachada (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}")

Edição

DocumentEditor.open abre um PDF existente para edições estruturais. Aqui limpamos os metadados e serializamos o resultado de volta para bytes.

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)

Próximos Passos