Skip to content

Начало работы с PDF Oxide (Zig)

PDF Oxide — самая быстрая PDF-библиотека со встроенным извлечением текста: 0.8 мс в среднем и 100% успешных проходов на 3830 PDF. Привязка для Zig — это идиоматичный Zig поверх C ABI pdf_oxide через @cImport: без промежуточных прослоек, с полноценным interop с C. Дескрипторы представлены структурами с методом deinit, а возвращаемые C-строки и буферы копируются в аллокатор, переданный вызывающим кодом.

Привязка зафиксирована на Zig 0.15.1 — до релиза 1.0 как сама сборка, так и API импорта C меняются от версии к версии, поэтому CI использует ровно эту же версию.

Установка

Привязка линкуется с cdylib, собранным с набором функций по умолчанию (а не с Python-колесом). Соберите нативную библиотеку, после чего укажите Zig путь к заголовку и к cdylib:

# 1. build the native library (shipped binding feature set)
cargo build --release --lib \
  --features ocr,rendering,signatures,barcodes,tsa-client,system-fonts

# 2. test + run the example
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"

В собственном коде достаточно импортировать модуль — и можно начинать работу:

const pdf_oxide = @import("pdf_oxide");

Открытие PDF

Откройте файл через Document.open (или Document.openFromBytes для данных в памяти) и изучите его метаданные. Каждый дескриптор владеет C-ресурсами, поэтому сопровождайте его вызовом 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()});
}

Извлечение текста

extractText возвращает текст одной страницы (нумерация с нуля). Результатом владеет переданный вами аллокатор, поэтому освобождайте его по завершении.

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});

Варианты для всего документа извлекают сразу все страницы:

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

Конвертация в Markdown и HTML

Преобразуйте отдельную страницу или весь документ в Markdown или HTML. Каждый метод возвращает срез, которым владеет аллокатор.

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

Извлечение на уровне слов

extractWords возвращает срез структур Word с текстом, ограничивающим прямоугольником, шрифтом и флагом жирности. Освобождайте весь срез парным помощником freeWords — он высвобождает как строки отдельных слов, так и сам срез.

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,
    });
}

Поля структуры Word:

Поле Тип Описание
text []u8 Текст слова (во владении аллокатора)
bbox Bbox { x, y, width, height } в пунктах
fontName []u8 Имя шрифта PostScript
fontSize f32 Размер шрифта в пунктах
bold bool Является ли фрагмент жирным

По той же схеме можно получить символы и строки:

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

Поиск

search ищет в пределах одной страницы; searchAll сканирует все страницы. Оба принимают NUL-терминированный запрос и флаг case_sensitive, а возвращают срез 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,
    });
}

Чтобы ограничить поиск одной страницей, используйте search с индексом страницы:

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

Создание PDF

Тип Pdf строит документы из Markdown, HTML или обычного текста. toBytes сериализует в память; save записывает на диск.

const a = std.heap.page_allocator;

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

// Serialize to memory...
const bytes = try pdf.toBytes(a);
defer a.free(bytes);

// ...or write straight to disk.
try pdf.save("output.pdf");

Только что собранный PDF можно сразу же прогнать обратно через экстрактор:

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});

Обработка ошибок

Методы, способные завершиться неудачно, возвращают Error!T, где Error — это error{ PdfOxide, OutOfMemory }. Поскольку значения ошибок в Zig не могут нести полезную нагрузку, исходный код ошибки из C ABI доступен через lastErrorCode() — читайте его сразу после перехвата 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);

Дальнейшие шаги