Skip to content

Empezar con PDF Oxide (Go)

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

A partir de la v0.3.38 se envían dos backends. Elige uno:

Opción A — CGo (enlace estático, predeterminado)

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

Requiere Go 1.21+ con CGO_ENABLED=1 (valor por defecto) y una toolchain de C en el PATH. Expone la API completa. El instalador descarga un archivo estático pdf_oxide-go-ffi-<platform>.tar.gz, verifica su SHA-256 e imprime los valores de CGO_CFLAGS y CGO_LDFLAGS que debes exportar. El núcleo Rust queda enlazado estáticamente, de modo que el binario resultante es autocontenido: no hace falta configurar LD_LIBRARY_PATH, DYLD_LIBRARY_PATH ni PATH en tiempo de ejecución. Basta con go build y listo.

Opción B — purego (sin toolchain de C, CGO_ENABLED=0)

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

Añadido en la v0.3.38 mediante ebitengine/purego. El instalador descarga un cdylib pdf_oxide-go-ffi-shared-<platform>.tar.gz (libpdf_oxide.so / .dylib / .dll) e imprime las variables de entorno que debes exportar:

export CGO_ENABLED=0
export PDF_OXIDE_LIB_PATH="$HOME/.cache/pdf_oxide/v0.3.38/lib/linux_amd64/libpdf_oxide.so"

La selección del backend es automática vía la etiqueta cgo integrada en Go: //go:build cgo → API CGo, //go:build !cgo → purego.

Superficie de purego (lo que compila bajo !cgo): apertura de PdfDocument (ruta / bytes / contraseña), recuento de páginas, versión, extracción de texto / Markdown / HTML / texto plano, API de Page, fuentes, anotaciones, elementos de página, búsqueda, dimensiones de página, logging, más PdfCreator.FromMarkdown / .FromHtml / .FromText para fixtures de prueba.

Solo CGo (error de compilación bajo !cgo): DocumentEditor, DocumentBuilder + FluentPageBuilder + EmbeddedFont, renderizado (RenderPage, RenderPageZoom, RenderThumbnail, RenderPageRegion, RenderPageFit), códigos de barras (GenerateQRCode, GenerateBarcode), firmas (Signatures, Signature.Verify), TSA (TsaClient), OCR (OcrEngine), y SetFormFieldValue / FlattenForms.

Flags del instalador

Flag Valor por defecto Propósito
-version versión compilada en el módulo Fijar una release concreta
-dir os.UserCacheDir()/pdf_oxide/v<ver> Cambiar el directorio de instalación
-shared off Descargar el cdylib (backend purego) en lugar del staticlib
-write-flags vacío (solo imprime variables) Directorio donde generar un cgo_flags.go
-env-only off Omite la descarga; solo imprime variables para una instalación existente
-skip-checksum off Omite la verificación SHA-256 (no recomendado)

Ubicaciones de caché (v0.3.38+)

La raíz de instalación pasó a os.UserCacheDir() para alinearse con la convención GOCACHE del propio Go:

SO Ruta
Linux $XDG_CACHE_HOME/pdf_oxide o ~/.cache/pdf_oxide
macOS ~/Library/Caches/pdf_oxide
Windows %LocalAppData%\pdf_oxide

Actualización desde v0.3.30 – v0.3.37: el primer go build fallará al enlazar (undefined reference to pdf_document_open ...) hasta que ejecutes el instalador una vez contra la nueva ruta. El antiguo directorio ~/.pdf_oxide/ no se migra automáticamente; bórralo a mano si quieres recuperar espacio en disco.

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.

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 (funciona bajo purego)
pdf, _ := pdfoxide.FromMarkdown("# Hola\n\nCuerpo del texto.")
defer pdf.Close()
pdf.Save("out.pdf")

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

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

// Solo CGo a partir de aquí:

// 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)

DocumentBuilder (solo CGo, v0.3.38)

La API fluida DocumentBuilder llega a Go en la v0.3.38. Aquí se incluyen anotaciones, widgets AcroForm (TextField, Checkbox, ComboBox, RadioGroup, PushButton), primitivas gráficas (Rect, FilledRect, Line), fuentes embebidas (CJK / cirílico / griego) y cifrado AES-256:

font, _ := pdfoxide.EmbeddedFontFromFile("DejaVuSans.ttf")
defer font.Close()

builder := pdfoxide.NewDocumentBuilder()
builder.RegisterEmbeddedFont("DejaVu", font)
builder.A4Page().
    Font("DejaVu", 12).At(72, 720).Text("Privet, mir!").
    Highlight(1.0, 1.0, 0.0).
    TextField("name", 150, 680, 200, 20, "Jane Doe").
    Checkbox("subscribe", 72, 650, 15, 15, true).
    Done()
_ = builder.SaveEncrypted("out.pdf", "user-pw", "owner-pw")

Consulta API fluida de DocumentBuilder para la superficie de métodos completa (la misma forma en todos los bindings).

Renderizado

Todas las APIs de renderizado son solo CGo (error de compilación bajo CGO_ENABLED=0).

// 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()

// Región recortada (v0.3.38)
region, _ := doc.RenderPageRegion(0, 72, 200, 468, 300, 0)
defer region.Close()

// Ajustar a un cuadro objetivo (v0.3.38)
fitted, _ := doc.RenderPageFit(0, 1024, 768, 0)
defer fitted.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

DocumentEditor es solo CGo. Úsalo 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

La generación de códigos de barras es solo CGo.

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