Primeros pasos con PDF Oxide (Scala)
PDF Oxide es la librería de PDF más rápida para la JVM con extracción de texto integrada: 0,8 ms de media y 100 % de aciertos sobre 3.830 PDF. El binding para Scala 3 es una fachada fina e idiomática sobre el maduro binding de Java: no añade código nativo y aporta extension methods de Scala que convierten java.util.Optional[T] en Option[T] y java.util.List[T] en Seq[T]. Los handles AutoCloseable funcionan directamente con scala.util.Using.
Instalación
Añade la dependencia a tu build.sbt:
libraryDependencies += "fyi.oxide" % "pdf-oxide" % "0.3.69"
La fachada de Scala depende del binding de Java fyi.oxide:pdf-oxide, que es el dueño del único puente nativo JNI. Se requiere Scala 3.3+.
Inicio rápido
Construye un PDF a partir de Markdown, luego ábrelo y vuelve a extraer el texto. Using.resource cierra cada handle por ti.
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 devuelve un handle Pdf; pdf.save() lo serializa a un Array[Byte]. PdfDocument.open acepta esos bytes y expone la API del documento.
Extracción de texto
Texto plano
Extrae texto plano de cualquier página por su índice de base cero.
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 y HTML
Convierte todo el documento a Markdown o HTML en una sola llamada.
import fyi.oxide.pdf.PdfDocument
import scala.util.Using
Using.resource(PdfDocument.open(pdfBytes)): doc =>
println(doc.toMarkdown())
println(doc.toHtml())
Elementos de página
doc.page(i) devuelve un PdfPage. La fachada expone cada extractor de elementos como un Seq de Scala mediante los extension methods *Seq: wordsSeq, linesSeq, charsSeq, tablesSeq, imagesSeq y annotationsSeq. Cada TextWord lleva su text y un 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}")
También puedes recorrer todas las páginas como un Seq con doc.pagesSeq (su tamaño coincide con 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")
}
Búsqueda
doc.searchSeq(query) devuelve un Seq[SearchMatch]. Cada coincidencia expone su 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}"))
Metadatos como Option
Los metadatos nulables del documento se exponen como Option[String] a través de producerOption y creatorOption, de modo que gestiones los valores ausentes al estilo de 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)"))
// Los campos de formulario también vuelven como un Seq:
println(s"form fields: ${doc.formFieldsSeq.size}")
Renderizado
doc.render(i) rasteriza una página y devuelve los bytes de la imagen 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)
Extracción automática
AutoExtractor.of(doc).extractDocument() devuelve un AutoResult con el text extraído, las representaciones opcionales en markdown/html y la lista de páginas que aún necesitan OCR, todo expuesto de forma idiomática a través de la 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}")
Edición
DocumentEditor.open abre un PDF existente para realizar ediciones estructurales. Aquí limpiamos los metadatos y volvemos a serializar el resultado a 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 pasos
- Primeros pasos con Rust – usar PDF Oxide desde Rust
- Primeros pasos con Python – usar PDF Oxide desde Python
- Extracción de texto – opciones y recetas de extracción en detalle
- Creación de PDF – creación avanzada, cifrado y metadatos
- Edición – modificar PDF existentes, anotaciones y campos de formulario