Skip to content

Biblioteca PDF para Go — PDF Oxide

PDF Oxide es la biblioteca PDF más rápida para Go: 0,8 ms promedio en extracción de texto, 5× más rápida que PyMuPDF, 15× más rápida que pypdf y 100 % de éxito en 3 830 PDFs. Un solo módulo para extraer, crear y editar PDFs. Lecturas seguras entre goroutines vía sync.RWMutex. Licencia MIT / Apache-2.0.

Instalación

go get github.com/yfedoseev/pdf_oxide/go
go run github.com/yfedoseev/pdf_oxide/go/cmd/install@latest

Requisitos: Go 1.21+ con CGo habilitado (CGO_ENABLED=1, valor por defecto).

Desde la v0.3.31, el archivo FFI nativo se descarga bajo demanda en vez de quedar versionado en el módulo. El comando install baja el tarball correspondiente a tu plataforma (~26 MB) a ~/.pdf_oxide/v<version>/, verifica su SHA-256 contra el .sha256 publicado e imprime los valores de CGO_CFLAGS y CGO_LDFLAGS que debes exportar. Pasa --write-flags=<dir> para generar un archivo cgo_flags.go junto a tu código en lugar de exportar variables de entorno. Basta con ejecutarlo una vez por máquina; @latest resuelve la versión correcta a través de runtime/debug.ReadBuildInfo.

El binding enlaza el núcleo Rust como biblioteca estática, de modo que el binario Go final queda completamente autocontenido: no hace falta configurar LD_LIBRARY_PATH, DYLD_LIBRARY_PATH ni PATH en tiempo de ejecución. Ejecuta go build y despliega.

Monorepo o builds desde el árbol de fuentes: añade -tags pdf_oxide_dev para que CGo apunte a un target/release/libpdf_oxide.a local — sin necesidad de instalador.

Actualización desde v0.3.30 o anterior: los archivos nativos versionados en go/lib/ ya no existen. El primer go build después de actualizar fallará al enlazar (undefined reference to pdf_document_open ...) hasta que ejecutes go run github.com/yfedoseev/pdf_oxide/go/cmd/install@latest y exportes las variables CGO_* que imprime (o las escribas con --write-flags).

Plataformas con binarios preconstruidos: Linux x64/arm64, macOS x64/arm64 (Apple Silicon) y Windows x64 (vía x86_64-pc-windows-gnu).

Abrir un PDF

package main

import (
    "fmt"
    "log"

    pdfoxide "github.com/yfedoseev/pdf_oxide/go"
)

func main() {
    doc, err := pdfoxide.Open("research-paper.pdf")
    if err != nil {
        log.Fatal(err)
    }
    defer doc.Close()

    count, _ := doc.PageCount()
    major, minor, _ := doc.Version()
    fmt.Printf("%d páginas, PDF %d.%d\n", count, major, minor)
}

API de páginas

Desde la v0.3.34 puedes trabajar primero a nivel de página. doc.Page(i) devuelve un handle liviano *Page que delega en el documento padre.

page, _ := doc.Page(0)
text, _ := page.Text()
md, _   := page.Markdown()

pages, _ := doc.Pages()
for _, p := range pages {
    t, _ := p.Text()
    fmt.Printf("--- Página %d ---\n%s\n", p.Index+1, t)
}

Cada Page expone Text(), Markdown(), Html(), PlainText(), Chars(), Words(), Lines(), Tables(), Images(), Paths(), Fonts(), Annotations(), Info(), Search(), NeedsOcr() y TextWithOcr().

Extracción de texto

Una sola página

text, err := doc.ExtractText(0)
if err != nil {
    log.Fatal(err)
}
fmt.Println(text)

Todas las páginas

allText, err := doc.ExtractAllText()
if err != nil {
    log.Fatal(err)
}
fmt.Println(allText)

Recorrer páginas manualmente

pages, _ := doc.Pages()
for _, p := range pages {
    text, err := p.Text()
    if err != nil {
        log.Printf("página %d: %v", p.Index, err)
        continue
    }
    fmt.Printf("--- Página %d ---\n%s\n", p.Index+1, text)
}

Extracción estructurada

words, _  := doc.ExtractWords(0)        // []Word
lines, _  := doc.ExtractTextLines(0)    // []TextLine
chars, _  := doc.ExtractChars(0)        // []Char
tables, _ := doc.ExtractTables(0)       // []Table — filas + celdas con bboxes (v0.3.34)
paths, _  := doc.ExtractPaths(0)        // []Path

for _, w := range words {
    fmt.Printf("%q en (%.1f, %.1f)\n", w.Text, w.X, w.Y)
}

for _, t := range tables {
    fmt.Printf("%dx%d (encabezado=%v)\n", t.RowCount, t.ColCount, t.HasHeader)
    for r := 0; r < t.RowCount; r++ {
        for c := 0; c < t.ColCount; c++ {
            fmt.Printf("%s\t", t.CellText(r, c))
        }
        fmt.Println()
    }
}

Extracción por región:

region, _ := doc.ExtractTextInRect(0, 50, 700, 200, 50) // x, y, ancho, alto
words, _  := doc.ExtractWordsInRect(0, 50, 700, 200, 50)

Conversión a Markdown

md, err := doc.ToMarkdown(0)
if err != nil {
    log.Fatal(err)
}
fmt.Println(md)

// Todas las páginas
allMd, _ := doc.ToMarkdownAll()

Conversión a HTML

html, _  := doc.ToHtml(0)
allHtml, _ := doc.ToHtmlAll()

Extracción de imágenes

import "os"

images, err := doc.Images(0)
if err != nil {
    log.Fatal(err)
}

for i, img := range images {
    fmt.Printf("Imagen %d: %dx%d %s %s %dbpc (%d bytes)\n",
        i, img.Width, img.Height, img.Format, img.Colorspace, img.BitsPerComponent, len(img.Data))
    os.WriteFile(fmt.Sprintf("image_%d.%s", i, img.Format), img.Data, 0644)
}

Abrir desde bytes y readers

// Desde bytes
data, _ := os.ReadFile("document.pdf")
doc, err := pdfoxide.OpenFromBytes(data)

// Desde cualquier io.Reader
doc, err := pdfoxide.OpenReader(someReader)

// Con contraseña
doc, err := pdfoxide.OpenWithPassword("secure.pdf", "user-password")

Creación de PDFs

// Desde Markdown
pdf, _ := pdfoxide.FromMarkdown("# Hola\n\nCuerpo del texto.")
defer pdf.Close()
pdf.Save("out.pdf")

// Desde HTML
htmlPdf, _ := pdfoxide.FromHtml("<h1>Factura</h1><p>Importe: $42</p>")
defer htmlPdf.Close()
htmlPdf.Save("invoice.pdf")

// Desde texto
txt, _ := pdfoxide.FromText("Documento de texto plano.")
defer txt.Close()

// Desde una imagen
img, _ := pdfoxide.FromImage("photo.jpg")
defer img.Close()

// Combinar varios PDFs
merged, _ := pdfoxide.Merge([]string{"a.pdf", "b.pdf"})
os.WriteFile("merged.pdf", merged, 0644)

Renderizado

// Formato: 0 = PNG, 1 = JPEG
img, err := doc.RenderPage(0, 0)
if err != nil {
    log.Fatal(err)
}
defer img.Close()
img.SaveToFile("page.png")

// Zoom (2×)
zoomed, _ := doc.RenderPageZoom(0, 2.0, 0)
defer zoomed.Close()

// Miniatura (200 px de ancho)
thumb, _ := doc.RenderThumbnail(0, 200, 0)
defer thumb.Close()

Búsqueda

// Buscar en todas las páginas (sin distinguir mayúsculas)
hits, _ := doc.SearchAll("configuration", false)
for _, r := range hits {
    fmt.Printf("página %d: %q en (%.0f, %.0f)\n", r.Page, r.Text, r.X, r.Y)
}

// Buscar en una sola página
pageHits, _ := doc.SearchPage(0, "configuration", false)

Edición

Usa DocumentEditor para metadatos, operaciones sobre páginas, anotaciones y formularios:

editor, err := pdfoxide.OpenEditor("in.pdf")
if err != nil {
    log.Fatal(err)
}
defer editor.Close()

// Metadatos — un campo a la vez
_ = editor.SetTitle("Informe trimestral")
_ = editor.SetAuthor("Equipo de Finanzas")

// O varios campos a la vez
_ = editor.ApplyMetadata(pdfoxide.Metadata{
    Title:   "Informe Q1 2026",
    Author:  "Equipo de Finanzas",
    Subject: "Resultados",
})

// Operaciones de página
_ = editor.SetPageRotation(0, 90)
_ = editor.MovePage(2, 0)
_ = editor.DeletePage(5)

// Formularios
_ = editor.SetFormFieldValue("employee.name", "Jane Doe")
_ = editor.FlattenForms()

// Guardar
_ = editor.Save("out.pdf")
_ = editor.SaveEncrypted("secret.pdf", "user", "owner")

Códigos de barras

qr, _ := pdfoxide.GenerateQRCode("https://example.com", 0, 256)
defer qr.Close()
_ = os.WriteFile("qr.png", qr.PNGData(), 0644)

bc, _ := pdfoxide.GenerateBarcode("123456789", 0, 128)
defer bc.Close()

OCR

Compila con la feature ocr para habilitar OCR sobre páginas escaneadas:

go build -tags ocr ./...
ocr, _ := pdfoxide.NewOcrEngine()
defer ocr.Close()

if ocr.NeedsOcr(doc, 0) {
    text, _ := ocr.ExtractTextWithOcr(doc, 0)
    fmt.Println(text)
}

Consulta la guía de OCR para recetas completas.

Concurrencia

Las lecturas sobre PdfDocument son seguras entre goroutines: varias goroutines pueden compartir un mismo documento para extraer páginas en paralelo.

import "sync"

var wg sync.WaitGroup
count, _ := doc.PageCount()
out := make(chan string, count)

for i := 0; i < count; i++ {
    wg.Add(1)
    go func(page int) {
        defer wg.Done()
        text, err := doc.ExtractText(page)
        if err == nil {
            out <- text
        }
    }(i)
}

go func() { wg.Wait(); close(out) }()

for text := range out {
    _ = text
}

DocumentEditor serializa las escrituras de forma interna, pero no canalices ediciones independientes desde varias goroutines: junta los cambios en una sola goroutine. Consulta los patrones en la guía de concurrencia.

Manejo de errores

import "errors"

text, err := doc.ExtractText(0)
if err != nil {
    switch {
    case errors.Is(err, pdfoxide.ErrDocumentClosed):
        log.Print("el documento está cerrado")
    case errors.Is(err, pdfoxide.ErrInvalidPageIndex):
        log.Print("índice de página inválido")
    case errors.Is(err, pdfoxide.ErrExtractionFailed):
        log.Print("falló la extracción")
    default:
        log.Printf("inesperado: %v", err)
    }
}

Errores centinela disponibles:

ErrInvalidPath        ErrDocumentNotFound   ErrInvalidFormat
ErrExtractionFailed   ErrParseError         ErrInvalidPageIndex
ErrSearchFailed       ErrInternal           ErrDocumentClosed
ErrEditorClosed       ErrCreatorClosed      ErrIndexOutOfBounds
ErrEmptyContent

Obtén el Code numérico y el Message con errors.As:

var e *pdfoxide.Error
if errors.As(err, &e) {
    fmt.Printf("code=%d message=%s\n", e.Code, e.Message)
}

Próximos pasos