Skip to content

Go PDF 库 — PDF Oxide

PDF Oxide 是 Go 最快的 PDF 库:文本提取平均 0.8 ms,比 PyMuPDF 快 5 倍,比 pypdf 快 15 倍,在 3830 个 PDF 上 100% 通过率。一个模块覆盖提取、生成、编辑;读取操作通过 sync.RWMutex 保证 goroutine 安全。MIT / Apache-2.0 双协议。

安装

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

环境要求: Go 1.21+,并启用 CGo(默认 CGO_ENABLED=1)。

从 v0.3.31 起,原生 FFI 归档改为按需下载,不再随模块提交。install 命令会把与当前平台匹配的压缩包(约 26 MB)下载到 ~/.pdf_oxide/v<version>/,用发布的 .sha256 校验 SHA-256,然后打印需要导出的 CGO_CFLAGSCGO_LDFLAGS。若希望直接在代码旁生成 cgo_flags.go 而不导出环境变量,可以加 --write-flags=<dir>。每台机器执行一次即可,@latest 会通过 runtime/debug.ReadBuildInfo 自动解析出对应的发行版。

绑定将 Rust 内核以静态库方式链接,因此最终的 Go 二进制完全自包含,运行时无需配置 LD_LIBRARY_PATHDYLD_LIBRARY_PATHPATHgo build 之后即可分发。

Monorepo 或源码树构建: 添加 -tags pdf_oxide_dev,让 CGo 指向本地的 target/release/libpdf_oxide.a,不需要安装脚本。

从 v0.3.30 或更早版本升级: 原先提交在 go/lib/ 下的原生归档已经删除。升级后第一次 go build 会在链接阶段失败(undefined reference to pdf_document_open ...),需要先执行一次 go run github.com/yfedoseev/pdf_oxide/go/cmd/install@latest,并导出它打印的 CGO_* 变量(或通过 --write-flags 写入)。

提供预编译的平台: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)
}

从字节与 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
pdf, _ := pdfoxide.FromMarkdown("# 你好\n\n正文内容。")
defer pdf.Close()
pdf.Save("out.pdf")

// 从 HTML
htmlPdf, _ := pdfoxide.FromHtml("<h1>发票</h1><p>金额: $42</p>")
defer htmlPdf.Close()
htmlPdf.Save("invoice.pdf")

// 从文本
txt, _ := pdfoxide.FromText("纯文本文档。")
defer txt.Close()

// 从图像
img, _ := pdfoxide.FromImage("photo.jpg")
defer img.Close()

// 合并多个 PDF
merged, _ := pdfoxide.Merge([]string{"a.pdf", "b.pdf"})
os.WriteFile("merged.pdf", merged, 0644)

渲染

// 格式: 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()

// 缩略图 (宽 200px)
thumb, _ := doc.RenderThumbnail(0, 200, 0)
defer thumb.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 处理元数据、页面操作、注释与表单:

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

// 元数据 — 逐字段设置
_ = editor.SetTitle("季度报告")
_ = editor.SetAuthor("财务团队")

// 或一次性设置多个字段
_ = editor.ApplyMetadata(pdfoxide.Metadata{
    Title:   "2026 年 Q1 报告",
    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")

条码

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 feature 构建:

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

可用的哨兵错误:

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

errors.As 取出数字 CodeMessage

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

下一步