Skip to content

Primeros pasos con PDF Oxide (Zig)

PDF Oxide es la biblioteca de PDF más rápida con extracción de texto integrada: 0,8 ms de media y 100 % de aciertos sobre 3830 PDFs. El binding de Zig es Zig idiomático sobre la C ABI de pdf_oxide mediante @cImport, sin capa intermedia y con interoperabilidad C de primera clase. Los handles son structs con deinit, y las cadenas y buffers de C devueltos se copian en un allocator que proporciona quien llama.

Fijado a Zig 0.15.1: al tratarse de una versión previa a la 1.0, tanto la compilación como la API de importación de C cambian entre versiones, por lo que CI usa exactamente la misma.

Instalación

El binding enlaza con la cdylib de características por defecto (no con el wheel de Python). Compila la biblioteca nativa y luego indícale a Zig dónde están la cabecera y la cdylib:

# 1. compila la biblioteca nativa (conjunto de características del binding)
cargo build --release --lib \
  --features ocr,rendering,signatures,barcodes,tsa-client,system-fonts

# 2. ejecuta los tests + el ejemplo
cd zig
LD_LIBRARY_PATH="$PWD/../target/release" \
  zig build test \
    -DPDF_OXIDE_INCLUDE_DIR="$PWD/../include" \
    -DPDF_OXIDE_LIB_DIR="$PWD/../target/release"

LD_LIBRARY_PATH="$PWD/../target/release" \
  zig build example \
    -DPDF_OXIDE_INCLUDE_DIR="$PWD/../include" \
    -DPDF_OXIDE_LIB_DIR="$PWD/../target/release"

En tu propio código, importa el módulo y listo:

const pdf_oxide = @import("pdf_oxide");

Abrir un PDF

Abre un archivo con Document.open (o Document.openFromBytes para datos en memoria) e inspecciona sus metadatos. Cada handle posee recursos de C, así que acompáñalo siempre con defer doc.deinit().

const std = @import("std");
const pdf_oxide = @import("pdf_oxide");

pub fn main() !void {
    const a = std.heap.page_allocator;

    var doc = try pdf_oxide.Document.open("research-paper.pdf");
    defer doc.deinit();

    std.debug.print("pages:   {d}\n", .{try doc.pageCount()});
    const v = doc.version();
    std.debug.print("version: {d}.{d}\n", .{ v.major, v.minor });
    std.debug.print("encrypted: {}\n", .{doc.isEncrypted()});
}

Extracción de texto

extractText devuelve el texto de una sola página (índice basado en 0). El resultado pertenece al allocator que le pasas, así que libéralo cuando termines.

const a = std.heap.page_allocator;

var doc = try pdf_oxide.Document.open("report.pdf");
defer doc.deinit();

const text = try doc.extractText(a, 0);
defer a.free(text);
std.debug.print("{s}\n", .{text});

Las variantes que abarcan todo el documento extraen todas las páginas de una vez:

const all_text = try doc.toPlainTextAll(a);
defer a.free(all_text);
std.debug.print("{s}\n", .{all_text});

Conversión a Markdown y HTML

Convierte una sola página o el documento entero a Markdown o HTML. Cada llamada devuelve un slice propiedad del allocator.

const md = try doc.toMarkdown(a, 0);
defer a.free(md);
std.debug.print("{s}\n", .{md});

const md_all = try doc.toMarkdownAll(a);
defer a.free(md_all);

const html = try doc.toHtml(a, 0);
defer a.free(html);

Extracción a nivel de palabra

extractWords devuelve un slice de structs Word con texto, bounding box, fuente e indicador de negrita. Libera el slice completo con el helper freeWords correspondiente: este libera las cadenas de cada palabra y el slice que las respalda.

const words = try doc.extractWords(a, 0);
defer pdf_oxide.Document.freeWords(a, words);

for (words) |w| {
    std.debug.print("'{s}' at ({d:.1}, {d:.1}) font={s} size={d:.1} bold={}\n", .{
        w.text, w.bbox.x, w.bbox.y, w.fontName, w.fontSize, w.bold,
    });
}

Campos de Word:

Campo Tipo Descripción
text []u8 Texto de la palabra (propiedad del allocator)
bbox Bbox { x, y, width, height } en puntos
fontName []u8 Nombre PostScript de la fuente
fontSize f32 Tamaño de la fuente en puntos
bold bool Si el fragmento está en negrita

El mismo patrón te da los caracteres y las líneas:

const chars = try doc.extractChars(a, 0);
defer pdf_oxide.Document.freeChars(a, chars);

const lines = try doc.extractTextLines(a, 0);
defer pdf_oxide.Document.freeTextLines(a, lines);

Búsqueda

search busca dentro de una sola página; searchAll recorre todas las páginas. Ambas reciben un término terminado en NUL y un indicador case_sensitive, y devuelven un slice de SearchResult.

const hits = try doc.searchAll(a, "configuration", false);
defer pdf_oxide.Document.freeSearchResults(a, hits);

for (hits) |hit| {
    std.debug.print("page {d}: '{s}' at ({d:.0}, {d:.0})\n", .{
        hit.page, hit.text, hit.bbox.x, hit.bbox.y,
    });
}

Para restringir la búsqueda a una sola página, usa search con el índice de la página:

const page_hits = try doc.search(a, 0, "Alpha", false);
defer pdf_oxide.Document.freeSearchResults(a, page_hits);

Crear un PDF

El tipo Pdf construye documentos a partir de Markdown, HTML o texto plano. toBytes serializa en memoria; save escribe en disco.

const a = std.heap.page_allocator;

var pdf = try pdf_oxide.Pdf.fromMarkdown("# Hello\n\nThis is a **Zig** PDF.\n");
defer pdf.deinit();

// Serializa en memoria...
const bytes = try pdf.toBytes(a);
defer a.free(bytes);

// ...o escribe directamente en disco.
try pdf.save("output.pdf");

Puedes hacer un ciclo completo: pasar un PDF recién creado de vuelta por el extractor:

var pdf = try pdf_oxide.Pdf.fromHtml("<h1>Invoice</h1><p>Amount: $42</p>");
defer pdf.deinit();

const bytes = try pdf.toBytes(a);
defer a.free(bytes);

var doc = try pdf_oxide.Document.openFromBytes(bytes);
defer doc.deinit();

const text = try doc.extractText(a, 0);
defer a.free(text);
std.debug.print("{s}\n", .{text});

Manejo de errores

Las llamadas que pueden fallar devuelven Error!T, donde Error es error{ PdfOxide, OutOfMemory }. Como los valores de error de Zig no pueden llevar información adicional, el código de la C ABI subyacente se expone a través de lastErrorCode(): léelo justo después de capturar error.PdfOxide.

const text = doc.extractText(a, 99) catch |err| switch (err) {
    error.PdfOxide => {
        std.debug.print("pdf_oxide error code: {d}\n", .{pdf_oxide.lastErrorCode()});
        return;
    },
    error.OutOfMemory => return err,
};
defer a.free(text);

Próximos pasos