easypdf-rust
An idiomatic Rust library for quick PDF operations.
Inspired by Alibaba EasyExcel's builder-pattern API design.

English · 简体中文
定位 · 功能 · 架构 ·
快速开始 · Markdown · Features ·
质量 · 路线图 · 贡献
Current Version: 0.1.0
MSRV: Rust 1.88
Edition: 2024
Workspace Resolver: 3
Maturity: Experimental (API and behavior may change)
License: Apache-2.0
Last Verified: 2026-08-09
1. Project Positioning & Status
1.1 What it is
easypdf-rust is an idiomatic Rust workspace for quick PDF operations — creation, reading, manipulation, template filling, and PDF → Markdown conversion.
| Dimension |
Detail |
| Root crate |
easypdf |
| Current version |
0.1.0 |
| MSRV / Edition |
1.88 / 2024 |
| Default features |
markdown |
| unsafe policy |
#![forbid(unsafe_code)] in every crate |
| Release status |
Workspace-only (not yet on crates.io) |
| License |
Apache-2.0 |
1.2 What it is NOT
- Not a 1:1 port of Java EasyExcel — PDF and Excel are fundamentally different paradigms.
- Does not claim implemented features that only return
UnsupportedFeature or produce stub output.
- Does not enable encryption or signing — these return explicit errors, not simulated success.
- Does not perform real OCR, table detection, or image extraction — the markdown pipeline emits structured warnings for these gaps.
1.3 Status Evidence
| Claim |
Value |
Evidence |
| Workspace builds |
✅ |
cargo check -p easypdf --all-features |
| Tests pass |
136 passed, 1 ignored (legacy trybuild) |
cargo test --workspace --quiet |
| New crates pass clippy |
✅ |
clippy -D warnings on easypdf-model, easypdf-io, easypdf-markdown |
| No-default-features builds |
✅ |
cargo check -p easypdf --no-default-features |
| Docs build |
✅ |
cargo doc --workspace --no-deps |
| Reader session reuse |
~129x faster than re-open |
cargo bench -p easypdf-reader --bench reader_session |
| crates.io |
Not published |
Workspace-only manifest |
2. Features & Maturity
2.1 Feature Matrix
| Feature |
Status |
Crate / Feature |
Limitations |
Verification |
| Create PDF (text, fonts, metadata) |
✅ Stable |
easypdf-writer |
Built-in fonts only by default |
Tests + examples |
| Read / extract text + metadata |
✅ Stable |
easypdf-reader |
Text extraction depends on font encoding |
Tests + benchmark |
| Streaming read strategy |
✅ Preview |
easypdf-reader |
ReadStrategy::Streaming for large files |
Strategy tests |
| PDF → Markdown |
✅ Preview |
easypdf-markdown / markdown |
Native text MVP; tables/images/OCR emit warnings |
6 profile tests |
| Merge / Split / Rotate / Reorder |
✅ Stable |
easypdf-reader::manipulate |
Valid /Pages tree on output |
Manipulate tests |
| Fill AcroForm fields |
✅ Stable |
easypdf-writer::template |
Field name matching |
Template tests |
#[derive(PdfModel)] |
✅ Stable |
easypdf-derive |
Extended attrs: field/order/skip/default/required/format/nested |
trybuild + integration tests |
| Table detection |
✅ Preview |
easypdf-markdown::table |
Pipe, tab, whitespace patterns |
Integration tests |
| OCR pipeline |
✅ Preview |
easypdf-markdown::ocr |
Trait-based OcrEngine abstraction |
Integration tests |
| Cloud OCR (GLM/Hunyuan/Baidu) |
✅ Preview |
easypdf-ocr |
Synchronous HTTP via reqwest |
Integration tests |
| Page rendering |
✅ Preview |
easypdf-markdown::render |
Text renderer (pdfium optional) |
Integration tests |
| Resident daemon |
✅ Preview |
easypdf-runtime / resident |
Unix socket IPC |
Integration tests |
| MCP server |
✅ Preview |
easypdf-runtime / mcp |
JSON-RPC over stdio for LLMs |
Unit tests |
| Writer backend selection |
✅ Stable |
easypdf-writer |
InMemory / Spill / Auto |
Integration tests |
| Writer lifecycle hooks |
✅ Stable |
easypdf-writer |
PdfWriteHandler trait |
Handler lifecycle test |
| Event-driven read listeners |
✅ Stable |
easypdf-reader |
PdfReadListener trait |
Listener test |
| Atomic output |
✅ Stable |
easypdf-core::io |
Temp file + atomic rename |
All save operations |
| Resource limits |
✅ Stable |
easypdf-core::io |
File size, page count, text length |
Limit exceeded tests |
| Engine-neutral semantic model |
✅ Preview |
easypdf-core::model |
PdfBlock / PdfPageModel / PdfDocumentModel |
Markdown pipeline |
| tracing observability |
✅ Stable |
easypdf-core::logging |
Structured spans, env-filter, JSON |
All crates |
| Tables, images, shapes |
🚧 Planned |
— |
Not yet implemented |
v0.3 roadmap |
| Custom TTF/OTF fonts |
🚧 Planned |
— |
Partial: register_font_from_path exists |
v0.3 roadmap |
| Encryption |
⛔ Not implemented |
— |
Returns UnsupportedFeature |
Explicit error test |
| Digital signatures |
⛔ Not implemented |
— |
Returns UnsupportedFeature |
Explicit error test |
2.2 Status Definitions
| Status |
Definition |
| ✅ Stable |
Public API, tests, and documentation complete; behavior verified |
| 🧪 Preview |
Usable but API or behavior may change |
| 🚧 Partial |
Only explicitly listed subsets are available |
| 🗓️ Planned |
No callable implementation yet |
| ⛔ Not implemented |
Returns explicit error; will not silently simulate success |
3. Rust Baseline & Platform Support
3.1 Toolchain
| Item |
Value |
Source |
| MSRV |
1.88 |
workspace.package.rust-version |
| Edition |
2024 |
workspace.package.edition |
| Resolver |
3 |
workspace.resolver |
| unsafe |
forbid |
workspace.lints.rust.unsafe_code |
3.2 Platform
| Platform |
Status |
Notes |
| Linux (x86_64) |
✅ |
Primary CI target |
| macOS (ARM64 / x86_64) |
✅ |
Development platform |
| Windows |
Expected |
No blocking platform-specific code |
| WASM |
Not tested |
lopdf/printpdf may have constraints |
4. Document Processing Pipeline
Input PDF / bytes / path
│
▼
Resource limit check (file size, page count)
│
▼
lopdf parse → PdfReader session (single-parse, reusable)
│
├──► extract_text / extract_metadata
├──► PdfDocumentModel (engine-neutral IR)
│ │
│ ▼
│ MarkdownRenderer (GFM / LLM / Plain profiles)
│ │
│ ├──► Output .md file (atomic write)
│ └──► MarkdownExportReport + structured warnings
│
├──► Merge / Split / Rotate / Reorder
│ │
│ └──► Atomic output (temp + rename)
│
└──► Template fill (AcroForm fields)
│
└──► Atomic output
5. Workspace & Crate Architecture
5.1 Crate Map
flowchart TB
facade["easypdf\nEasyPdf facade"]
core["easypdf-core\ntypes + errors + model + io + layout"]
derive["easypdf-derive\nproc-macro"]
reader["easypdf-reader\nread + manipulate + streaming"]
writer["easypdf-writer\nwrite + template + backends"]
markdown["easypdf-markdown\nmarkdown + table + OCR + render"]
ocr["easypdf-ocr\ncloud OCR engines"]
runtime["easypdf-runtime\nMCP + resident daemon"]
test["easypdf-test\nintegration tests"]
facade --> markdown & reader & writer & ocr
runtime --> reader & writer & markdown
markdown --> reader & core
reader --> core
writer --> core
ocr --> markdown & core
derive --> core
test --> facade
5.2 Crate Responsibilities
| Crate |
Purpose |
Backend |
| easypdf |
Facade + EasyPdf entry point + all Builder types |
Depends on core, reader, writer, markdown, ocr |
| easypdf-core |
Types, enums, traits, PdfError, semantic IR, resource limits, atomic output, layout abstractions |
thiserror, chrono (no engine) |
| easypdf-derive |
#[derive(PdfModel)] proc-macro with extended field attributes |
syn, quote |
| easypdf-reader |
PDF parsing, text extraction, metadata, session reuse, merge/split/rotate/reorder, streaming strategy |
lopdf |
| easypdf-writer |
PDF creation with text, images, shapes, fonts, WriteBackend selection, AcroForm filling |
printpdf |
| easypdf-markdown |
PDF → Markdown with profiles, processor pipeline, table detection, OCR fallback, page rendering |
lopdf + easypdf-core |
| easypdf-ocr |
Cloud OCR engine collection (GLM, HunyuanOCR, Baidu) |
reqwest (blocking) |
| easypdf-runtime |
MCP server (easypdf-mcp binary) + resident daemon for in-memory sessions |
Feature-gated mcp / resident |
| easypdf-test |
Integration test harness with golden files and sample PDFs |
Test-only |
5.3 Dependency Rules
easypdf-core has zero engine dependencies — it is the shared vocabulary (types, model, io, layout).
easypdf-derive depends only on easypdf-core.
- Domain crates (reader, writer, markdown) do NOT depend on each other.
easypdf-ocr depends on easypdf-markdown (for the OcrEngine trait) and easypdf-core.
easypdf-runtime depends on reader, writer, and markdown for the MCP/resident daemons.
- Only the
easypdf facade depends on all sub-crates.
6. Cargo Features
| Feature |
Crates enabled |
Impact |
Default |
markdown |
easypdf-markdown |
PDF → Markdown pipeline |
✅ |
html |
printpdf/html |
HTML → PDF (requires Chromium) |
❌ |
markdown-table |
easypdf-markdown-table |
Table detection in markdown pipeline |
❌ |
markdown-ocr |
easypdf-markdown-ocr |
OCR fallback for scanned pages |
❌ |
ocr |
alias for markdown-ocr |
OCR (alias) |
❌ |
render |
easypdf-render |
PDF page rendering to PNG |
❌ |
resident |
easypdf-resident |
Resident daemon for in-memory sessions |
❌ |
mcp |
easypdf-mcp |
MCP server for LLM agents |
❌ |
full |
all optional crates |
Everything enabled |
❌ |
easypdf = "0.1.0"
easypdf = { version = "0.1.0", default-features = false }
easypdf = { version = "0.1.0", features = ["html"] }
easypdf = { version = "0.1.0", features = ["markdown-table"] }
easypdf = { version = "0.1.0", features = ["ocr"] }
easypdf = { version = "0.1.0", features = ["full"] }
7. Quick Start
7.1 Create a PDF
use easypdf::prelude::*;
EasyPdf::create("output.pdf")
.page(PageSize::A4)
.add_text("Hello, world!")
.font(PdfFont::helvetica(12.0))
.position(72.0, 700.0)
.do_write()?;
# Ok::<(), easypdf::PdfError>(())
7.2 Read a PDF
use easypdf::prelude::*;
let text = EasyPdf::read("input.pdf")
.pages(0..10)
.extract_text()?;
let meta = EasyPdf::read("input.pdf")
.extract_metadata()?;
# Ok::<(), easypdf::PdfError>(())
7.3 Merge PDFs
use easypdf::prelude::*;
EasyPdf::merge(&["a.pdf", "b.pdf", "c.pdf"], "merged.pdf")?;
# Ok::<(), easypdf::PdfError>(())
7.4 Split PDF
use easypdf::prelude::*;
EasyPdf::split("input.pdf")
.output_dir("/tmp/pages")
.do_split()?;
# Ok::<(), easypdf::PdfError>(())
7.5 Manipulate Pages
use easypdf::prelude::*;
EasyPdf::manipulate("input.pdf")
.rotate_all(Rotation::Clockwise90)
.reorder_pages(&[2, 0, 1])
.save("reordered.pdf")?;
# Ok::<(), easypdf::PdfError>(())
7.6 Fill a Form
use easypdf::prelude::*;
#[derive(PdfModel)]
struct MyData {
#[pdf(field = "name")]
name: String,
}
EasyPdf::fill_form("template.pdf", &MyData { name: "Alice".into() })
.save("filled.pdf")?;
# Ok::<(), easypdf::PdfError>(())
8. PDF → Markdown Export
The easypdf-markdown crate provides deterministic PDF → Markdown conversion with profile-based rendering, zero-based page ranges, export reports, and structured warnings.
8.1 Export API
use easypdf::prelude::*;
EasyPdf::export_markdown("input.pdf", "output.md")
.pages(0..20)
.profile(MarkdownProfile::Llm)
.tables(TablePolicy::Detect)
.ocr(OcrPolicy::Auto)
.do_export()?;
# Ok::<(), easypdf::PdfError>(())
8.2 Markdown Profiles
| Profile |
Target use case |
Output style |
MarkdownProfile::Gfm |
GitHub / GitLab rendering |
Standard GFM with tables and fenced blocks |
MarkdownProfile::Llm |
LLM context injection |
Clean, minimal markup optimized for token efficiency |
MarkdownProfile::Plain |
Human reading / plain text |
Minimal formatting, maximum readability |
8.3 Structured Warnings
When a capability is not yet implemented, the markdown pipeline emits structured warnings rather than simulating success:
use easypdf::prelude::*;
let report = EasyPdf::export_markdown("input.pdf", "output.md")
.do_export()?;
for warning in report.warnings() {
match warning {
MarkdownWarning::TableDetectionUnavailable { page } => { }
MarkdownWarning::ImageExtractionUnavailable { page } => { }
MarkdownWarning::OcrUnavailable { page } => { }
}
}
# Ok::<(), easypdf::PdfError>(())
8.4 Markdown Pipeline with Table Detection
use easypdf::prelude::*;
let mut pipeline = EasyPdf::markdown_pipeline(MarkdownProfile::Gfm);
pipeline.register(Box::new(EasyPdf::table_detector()));
# Ok::<(), easypdf::PdfError>(())
8.5 Render PDF Page to PNG
use easypdf::EasyPdf;
EasyPdf::render_page("input.pdf".as_ref(), 0, "page_0.png".as_ref(), 150)?;
# Ok::<(), easypdf::RenderError>(())
8.6 Writer with Backend Selection
use easypdf::prelude::*;
let writer = EasyPdf::writer("Big Report")
.backend(WriteBackend::default())
.build()?;
# Ok::<(), easypdf::PdfError>(())
8.7 Resident Daemon
use easypdf::EasyPdf;
EasyPdf::serve(None)?;
if let Some(client) = EasyPdf::attach() {
}
# Ok::<(), easypdf::PdfError>(())
8.8 MCP Server
use easypdf::EasyPdf;
let server = EasyPdf::mcp_server();
server.run()?;
# Ok::<(), easypdf::PdfError>(())
9. Core API Overview
9.1 Entry Points
| Method |
Returns |
Feature |
Purpose |
EasyPdf::create(path) |
PdfCreateBuilder |
— |
Build and write a new PDF |
EasyPdf::read(path) |
PdfReadBuilder |
— |
Extract text and metadata |
EasyPdf::export_markdown(input, output) |
PdfMarkdownExportBuilder |
markdown |
PDF → Markdown (file) |
EasyPdf::to_markdown(input) |
PdfMarkdownBuilder |
markdown |
PDF → Markdown (in-memory) |
EasyPdf::merge(&[paths], output) |
Result<()> |
— |
Merge multiple PDFs |
EasyPdf::split(path) |
PdfSplitBuilder |
— |
Split PDF into pages |
EasyPdf::manipulate(path) |
PdfManipulateBuilder |
— |
Rotate, reorder pages |
EasyPdf::fill_form(path, data) |
PdfFillBuilder |
— |
Fill AcroForm fields |
EasyPdf::writer(title) |
PdfWriterBuilder |
— |
Create PDF Writer with backend selection |
EasyPdf::markdown_pipeline(profile) |
ProcessorPipeline |
markdown |
Markdown conversion pipeline |
EasyPdf::table_detector() |
TableDetectorProcessor |
markdown-table |
Table detection processor |
EasyPdf::render_page(path, page, out, dpi) |
Result<(), RenderError> |
render |
Render PDF page to PNG |
EasyPdf::mcp_server() |
McpServer |
mcp |
Launch MCP server |
EasyPdf::serve(socket) |
Result<()> |
resident |
Start resident daemon |
EasyPdf::attach() |
Option<ResidentClient> |
resident |
Attach to running daemon |
EasyPdf::encrypt(input, output, pwd) |
Result<()> |
— |
⛔ Returns UnsupportedFeature |
EasyPdf::sign(input, output, reason) |
Result<()> |
— |
⛔ Returns UnsupportedFeature |
9.2 Reader Session Reuse
The PdfReader parses the document exactly once and retains it in memory. Subsequent operations on the same reader reuse the parsed session without re-opening the file.
Reader::open(path) → parse PDF once, retain Document
.pages(0..5) → filter page range (0-based)
.extract_text() → iterate selected pages
.extract_metadata() → read /Info dictionary
Benchmark (local, 3-page PDF):
| Operation |
Latency |
Ratio |
| Reuse parsed session |
~1,047 ns/iter |
1x |
| Re-open + re-parse |
~135,011 ns/iter |
~129x |
9.3 Traits
| Trait |
Role |
Analogy from EasyExcel |
PdfModel |
Map struct fields to PDF elements + form descriptors (derive) |
ExcelRow |
PdfReadListener |
Event-driven text extraction callbacks |
ReadListener<T> |
PdfWriteHandler |
Page lifecycle hooks (before/after page) |
WriteHandler |
PdfConverter<T> |
Bidirectional Rust ⇄ PDF string |
Converter<T> |
LayoutSink |
Backend-neutral layout consumption |
— |
OcrEngine |
OCR backend abstraction |
— |
PdfMarkdownProcessor |
Markdown pipeline processor |
— |
PdfRenderer |
PDF page rasterization |
— |
9.4 #[derive(PdfModel)] Field Attributes
| Attribute |
Effect |
#[pdf(text, position = (x, y))] |
Render as positioned text |
#[pdf(table, position = (x, y))] |
Render as table |
#[pdf(image, position = (x, y))] |
Render as image |
#[pdf(ignore)] / #[pdf(skip)] |
Skip field entirely |
#[pdf(field = "name")] |
Map to PDF form field name |
#[pdf(order = N)] |
Display/render order |
#[pdf(default = "value")] |
Default value when empty |
#[pdf(required)] |
Field must be non-empty |
#[pdf(format = "pattern")] |
Format pattern (e.g. "YYYY-MM-DD") |
#[pdf(nested)] |
Recursively render inner model |
10. Error Handling & Resource Limits
10.1 Error Type
pub enum PdfError {
Io(std::io::Error),
Parse(String),
InvalidPage(usize),
UnsupportedFeature(String),
ResourceLimitExceeded { resource: &'static str, limit: u64, actual: u64 },
Encryption(String),
Other(String),
}
pub type Result<T, E = PdfError> = std::result::Result<T, E>;
10.2 Resource Limits
All file and memory operations are bounded by ResourceLimits:
| Resource |
Default |
Exceeded behavior |
| Max file size |
100 MB |
ResourceLimitExceeded error |
| Max pages |
10,000 |
ResourceLimitExceeded error |
| Max text length |
10 MB |
ResourceLimitExceeded error |
10.3 Atomic Output
All save operations (Writer, Manipulate, Template, Markdown) use atomic output: write to a temporary file, then rename on success. If the operation fails, the original file is not corrupted.
11. Safety & Non-Goals
| Non-Goal |
Rationale |
| Encryption / signing |
Not implemented; returns UnsupportedFeature — no fake security |
| OCR |
Not implemented; markdown export emits OcrUnavailable warning |
| Table detection |
Not implemented; markdown export emits TableDetectionUnavailable warning |
| Image extraction |
Not implemented; markdown export emits ImageExtractionUnavailable warning |
| 1:1 Java EasyExcel compatibility |
PDF and Excel are different paradigms; API is inspired-by, not clone-of |
12. Roadmap
| Phase |
Focus |
Key Deliverables |
Status |
| v0.1 |
Foundation |
11 crates, core types, read/write/manipulate/template/markdown, derive macro, builder API, atomic output, resource limits |
✅ |
| v0.2 |
Architecture + OCR |
9-crate consolidation, streaming read, WriteBackend, cloud OCR (GLM/Hunyuan/Baidu), MCP, resident daemon, tracing |
✅ |
| v0.3 |
Rich content |
Tables, images, vector shapes, custom TTF/OTF fonts, page headers/footers |
🚧 |
| v0.4 |
Watermarks & layout |
Text/image watermarks, layout engine, PDF layers (OCG) |
🗓️ |
| v0.5 |
Security |
AES-256 encryption/decryption, password protection |
🗓️ |
| v0.6 |
Compliance |
PDF/A validation, digital signatures, XMP metadata |
🗓️ |
| v0.7 |
Converters |
HTML → PDF, Markdown → PDF, SVG → PDF |
🗓️ |
| v1.0 |
Stable |
Stable API, full test coverage, performance benchmarks |
🗓️ |
13. Performance & Benchmarks
cargo bench -p easypdf-reader --bench reader_session
| Scenario |
Data size |
Latency |
Notes |
| Session reuse (parsed in memory) |
3-page PDF |
~1,047 ns/iter |
Single parse, repeated access |
| Re-open + re-parse |
3-page PDF |
~135,011 ns/iter |
Full I/O + parse each time |
| Speedup |
— |
~129x |
Session reuse vs re-open |
Hardware: local development machine. Benchmark does not equal production SLA.
14. Build, Test & Quality Gates
14.1 Basic Gates
cargo check -p easypdf --no-default-features
cargo check -p easypdf --all-features
cargo test --workspace --quiet
cargo doc --workspace --no-deps
14.2 Extended Gates
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo clippy -p easypdf-model -p easypdf-io -p easypdf-markdown -- -D warnings
14.3 Test Matrix
| Type |
Scope |
Command |
| Unit tests |
All crates |
cargo test --workspace |
| Compile tests |
derive macro |
trybuild (1 ignored legacy test) |
| Doc tests |
API examples |
cargo test --doc |
| Feature combinations |
default, no-default, all |
cargo check variants |
15. Documents & Examples
16. Contributing & License
Before submitting, run all basic gates. New public API must include docs, examples, tests, and SemVer impact notes.
This project is licensed under Apache-2.0.
17. Related Projects
Back to top · docs.rs · crates.io · Issues