Skip to content

PDF Oxide をはじめる (Zig)

PDF Oxide は、テキスト抽出機能を標準で備えた最速の PDF ライブラリです。平均 0.8ms、3,830 件の PDF で 100% のパス率を達成しています。Zig バインディングは @cImport を介して pdf_oxide の C ABI 上に構築されたイディオマティックな Zig 実装で、シムを挟まず C との相互運用をファーストクラスで扱います。ハンドルは deinit を持つ struct であり、返される C の文字列やバッファは呼び出し側が渡したアロケータにコピーされます。

Zig 0.15.1 に固定しています。1.0 以前のビルドや C インポート API はリリースごとに変化するため、CI でも同じバージョンを使用しています。

インストール

このバインディングはデフォルトフィーチャの cdylib にリンクします (Python の wheel ではありません)。まずネイティブライブラリをビルドし、その後 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 は 1 ページ分 (0 始まり) のテキストを返します。結果は渡したアロケータが所有するため、使い終わったら解放してください。

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 への変換

1 ページまたはドキュメント全体を 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 struct のスライスを返します。スライス全体は対応する 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 は 1 ページ内を検索し、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,
    });
}

検索を 1 ページに限定するには、ページインデックスを指定して 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 を返します。ここで Errorerror{ 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);

次のステップ