Skip to content

PDF-бібліотека для Go — PDF Oxide

PDF Oxide — найшвидша PDF-бібліотека для Go: 0,8 мс на сторінку в середньому, у 5× швидше за PyMuPDF, у 15× швидше за pypdf, 100 % успіху на 3 830 PDF. Один модуль для видобування, створення й редагування. Читання безпечне між goroutine завдяки sync.RWMutex. Ліцензія MIT / Apache-2.0.

Встановлення

Починаючи з v0.3.38, прив’язка постачається у двох варіантах. Оберіть один:

Варіант A — CGo (статичне лінкування, типовий)

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

Потрібен Go 1.21+ із увімкненим CGO_ENABLED=1 (типово) та C-інструментарій у PATH. Повна поверхня API. Інсталятор завантажує статичний архів pdf_oxide-go-ffi-<platform>.tar.gz, перевіряє його через SHA-256 і друкує CGO_CFLAGS / CGO_LDFLAGS, які треба експортувати. Rust-ядро лінкується статично, тож готовий бінарник цілком самодостатній — на етапі виконання не потрібні ані LD_LIBRARY_PATH, ані DYLD_LIBRARY_PATH, ані PATH. Досить go build — і в продакшн.

Варіант B — purego (без C-інструментарію, CGO_ENABLED=0)

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

Додано у v0.3.38 через ebitengine/purego. Інсталятор завантажує cdylib pdf_oxide-go-ffi-shared-<platform>.tar.gz (libpdf_oxide.so / .dylib / .dll) і друкує змінні оточення, які потрібно експортувати:

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

Вибір бекенду відбувається автоматично через вбудований тег cgo: //go:build cgo → CGo API, //go:build !cgo → purego.

Поверхня purego (компілюється під !cgo): відкриття PdfDocument (шлях / байти / пароль), кількість сторінок, версія, видобування тексту / Markdown / HTML / plain-text, Page API, шрифти, анотації, елементи сторінки, пошук, розміри сторінок, логування, а також PdfCreator.FromMarkdown / .FromHtml / .FromText для тестових фікстур.

Лише CGo (помилка компіляції під !cgo): DocumentEditor, DocumentBuilder + FluentPageBuilder + EmbeddedFont, рендеринг (RenderPage, RenderPageZoom, RenderThumbnail, RenderPageRegion, RenderPageFit), штрихкоди (GenerateQRCode, GenerateBarcode), підписи (Signatures, Signature.Verify), TSA (TsaClient), OCR (OcrEngine), а також SetFormFieldValue / FlattenForms.

Прапорці інсталятора

Прапорець Типове значення Призначення
-version версія з модуля Прив’язати до конкретного релізу
-dir os.UserCacheDir()/pdf_oxide/v<ver> Перевизначити теку встановлення
-shared вимкнено Завантажити cdylib (бекенд purego) замість staticlib
-write-flags порожньо (лише друк env) Тека, куди згенерувати cgo_flags.go
-env-only вимкнено Пропустити завантаження; надрукувати env для наявної інсталяції
-skip-checksum вимкнено Пропустити перевірку SHA-256 (не рекомендовано)

Розташування кешу (v0.3.38+)

Теку встановлення перенесено в os.UserCacheDir(), щоб збігалася з конвенцією GOCACHE самого Go:

ОС Шлях
Linux $XDG_CACHE_HOME/pdf_oxide або ~/.cache/pdf_oxide
macOS ~/Library/Caches/pdf_oxide
Windows %LocalAppData%\pdf_oxide

Оновлення з v0.3.30 – v0.3.37: перший go build впаде на лінкуванні (undefined reference to pdf_document_open ...), доки інсталятор не відпрацює один раз у новому шляху. Стара тека ~/.pdf_oxide/ автоматично не мігрує; видаліть її вручну, якщо хочете звільнити диск.

Монорепо або складання з дерева вихідних кодів: додайте -tags pdf_oxide_dev, щоб CGo дивився на локальний target/release/libpdf_oxide.a — інсталятор при цьому не потрібен.

Платформи з готовими збірками: Linux x64/arm64, macOS x64/arm64 (Apple Silicon), Windows x64 (через x86_64-pc-windows-gnu).

Відкриття 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 сторінок, PDF %d.%d\n", count, major, minor)
}

API сторінок

З версії v0.3.34 можна працювати посторінково. doc.Page(i) повертає легкий *Page-дескриптор, який делегує виклики батьківському документу.

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

pages, _ := doc.Pages()
for _, p := range pages {
    t, _ := p.Text()
    fmt.Printf("--- Сторінка %d ---\n%s\n", p.Index+1, t)
}

Кожна Page надає Text(), Markdown(), Html(), PlainText(), Chars(), Words(), Lines(), Tables(), Images(), Paths(), Fonts(), Annotations(), Info(), Search(), NeedsOcr() та TextWithOcr().

Видобування тексту

Одна сторінка

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

Усі сторінки

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

Ручний обхід сторінок

pages, _ := doc.Pages()
for _, p := range pages {
    text, err := p.Text()
    if err != nil {
        log.Printf("сторінка %d: %v", p.Index, err)
        continue
    }
    fmt.Printf("--- Сторінка %d ---\n%s\n", p.Index+1, text)
}

Структуроване видобування

words, _  := doc.ExtractWords(0)        // []Word
lines, _  := doc.ExtractTextLines(0)    // []TextLine
chars, _  := doc.ExtractChars(0)        // []Char
tables, _ := doc.ExtractTables(0)       // []Table — рядки та комірки з bbox (v0.3.34)
paths, _  := doc.ExtractPaths(0)        // []Path

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

for _, t := range tables {
    fmt.Printf("%dx%d (заголовок=%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()
    }
}

Видобування за областю:

region, _ := doc.ExtractTextInRect(0, 50, 700, 200, 50) // x, y, ширина, висота
words, _  := doc.ExtractWordsInRect(0, 50, 700, 200, 50)

Конвертація в Markdown

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

// Усі сторінки
allMd, _ := doc.ToMarkdownAll()

Конвертація в HTML

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

Видобування зображень

import "os"

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

for i, img := range images {
    fmt.Printf("Зображення %d: %dx%d %s %s %dbpc (%d байт)\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)
}

Відкриття з байтів та io.Reader

// Із байтів
data, _ := os.ReadFile("document.pdf")
doc, err := pdfoxide.OpenFromBytes(data)

// З будь-якого io.Reader
doc, err := pdfoxide.OpenReader(someReader)

// З паролем
doc, err := pdfoxide.OpenWithPassword("secure.pdf", "user-password")

Створення PDF

// З Markdown (працює під purego)
pdf, _ := pdfoxide.FromMarkdown("# Привіт\n\nОсновний текст.")
defer pdf.Close()
pdf.Save("out.pdf")

// З HTML (працює під purego)
htmlPdf, _ := pdfoxide.FromHtml("<h1>Рахунок</h1><p>Сума: $42</p>")
defer htmlPdf.Close()
htmlPdf.Save("invoice.pdf")

// Із тексту (працює під purego)
txt, _ := pdfoxide.FromText("Простий текстовий документ.")
defer txt.Close()

// Нижче — лише CGo:

// Із зображення
img, _ := pdfoxide.FromImage("photo.jpg")
defer img.Close()

// Об'єднати кілька PDF
merged, _ := pdfoxide.Merge([]string{"a.pdf", "b.pdf"})
os.WriteFile("merged.pdf", merged, 0644)

DocumentBuilder (лише CGo, v0.3.38)

У v0.3.38 у Go з’явився вільний (fluent) API DocumentBuilder. Тут же — анотації, AcroForm-віджети (TextField, Checkbox, ComboBox, RadioGroup, PushButton), графічні примітиви (Rect, FilledRect, Line), вбудовані шрифти (CJK / кирилиця / грецька) і шифрування 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("Привіт, світе!").
    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")

Повну поверхню методів (однакову в усіх прив’язках) дивіться у Вільному API DocumentBuilder.

Рендеринг

Усі рендер-API доступні лише під CGo (помилка компіляції під CGO_ENABLED=0).

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

// Масштаб (2×)
zoomed, _ := doc.RenderPageZoom(0, 2.0, 0)
defer zoomed.Close()

// Ескіз (ширина 200 px)
thumb, _ := doc.RenderThumbnail(0, 200, 0)
defer thumb.Close()

// Обрізана область (v0.3.38)
region, _ := doc.RenderPageRegion(0, 72, 200, 468, 300, 0)
defer region.Close()

// Вписати в цільовий прямокутник (v0.3.38)
fitted, _ := doc.RenderPageFit(0, 1024, 768, 0)
defer fitted.Close()

Пошук

// Пошук у всіх сторінках (без урахування регістру)
hits, _ := doc.SearchAll("configuration", false)
for _, r := range hits {
    fmt.Printf("сторінка %d: %q у (%.0f, %.0f)\n", r.Page, r.Text, r.X, r.Y)
}

// Пошук на одній сторінці
pageHits, _ := doc.SearchPage(0, "configuration", false)

Редагування

DocumentEditor доступний лише під CGo. Використовуйте його для метаданих, операцій зі сторінками, анотацій і форм:

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

// Метадані — по одному полю
_ = editor.SetTitle("Квартальний звіт")
_ = editor.SetAuthor("Фінансовий відділ")

// Або кілька полів одразу
_ = editor.ApplyMetadata(pdfoxide.Metadata{
    Title:   "Звіт за Q1 2026",
    Author:  "Фінансовий відділ",
    Subject: "Результати",
})

// Операції зі сторінками
_ = editor.SetPageRotation(0, 90)
_ = editor.MovePage(2, 0)
_ = editor.DeletePage(5)

// Форми
_ = editor.SetFormFieldValue("employee.name", "Jane Doe")
_ = editor.FlattenForms()

// Збереження
_ = editor.Save("out.pdf")
_ = editor.SaveEncrypted("secret.pdf", "user", "owner")

Штрихкоди

Генерація штрихкодів доступна лише під 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

Щоб увімкнути OCR для сканованих сторінок, зберіть із фічею ocr:

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

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

Повні рецепти — у посібнику з OCR.

Паралелізм

Читання з PdfDocument безпечне між goroutine — кілька goroutine можуть ділити один документ і паралельно видобувати сторінки.

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 серіалізує записи всередині, але не перекачуйте незалежні правки з кількох goroutine у спільний канал — збирайте зміни в одній goroutine. Шаблони — у посібнику з паралелізму.

Обробка помилок

import "errors"

text, err := doc.ExtractText(0)
if err != nil {
    switch {
    case errors.Is(err, pdfoxide.ErrDocumentClosed):
        log.Print("документ закрито")
    case errors.Is(err, pdfoxide.ErrInvalidPageIndex):
        log.Print("некоректний індекс сторінки")
    case errors.Is(err, pdfoxide.ErrExtractionFailed):
        log.Print("не вдалося видобути текст")
    default:
        log.Printf("неочікувана помилка: %v", err)
    }
}

Доступні sentinel-помилки:

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

Числовий Code та Message витягаються через errors.As:

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

Наступні кроки