# Ovid - Development Notes
## Rules
- NEVER add Co-Authored-By lines to commits. No exceptions.
## Architecture
Rust CLI tool for PDF <-> Image conversion. Two commands:
- `ovid split <pdf>` — PDF pages to images (PNG/JPG) via MuPDF
- `ovid merge <images...>` — images to PDF via lopdf
## Workspace Structure
```
ovid/
├── Cargo.toml # workspace root + ovid binary
├── src/main.rs # CLI entry point (~1100 lines)
│ └── release.yml # CI: builds binaries on tag push
└── tests/
├── merge.rs # integration tests
└── data/ # test PDFs
```
## Distribution
**Published on:** crates.io, GitHub Releases
**Install methods:**
- Pre-built binaries: `curl -fsSL https://raw.githubusercontent.com/euceph/ovid/main/install.sh | sh`
- Cargo: `cargo install ovid`
- Source: clone + `cargo install --path .`
**Release workflow** (`.github/workflows/release.yml`):
- Triggers on `v*` tags or manual dispatch
- Builds 4 targets in parallel:
- `ovid-darwin-arm64` (macOS Apple Silicon, macos-15)
- `ovid-darwin-x86_64` (macOS Intel, macos-15-intel)
- `ovid-linux-x86_64` (ubuntu-latest)
- `ovid-linux-arm64` (ubuntu-24.04-arm)
- Creates GitHub Release with `.tar.gz` binaries on tag push
**To release a new version:**
```bash
git tag v0.x.x
git push --tags
```
## Build Dependencies
**macOS:**
```bash
brew install nasm jpeg-turbo
```
**Linux (Debian/Ubuntu):**
```bash
apt install cmake nasm libclang-dev libfontconfig1-dev libjpeg-turbo8-dev pkg-config
```
## Dependencies
- `mupdf` 0.6 — PDF rendering (vendors MuPDF C source)
- `turbojpeg` 1.3 — JPEG encoding (builds libjpeg-turbo from source, needs nasm)
- `image` 0.25 — image encoding/decoding (PNG, JPEG, TIFF, BMP, GIF)
- `png` 0.18 — PNG encoding with filter control
- `lopdf` 0.34 — PDF creation from images
- `clap` 4 + `clap_complete` — CLI parsing + shell completions
- `rayon` 1 — parallel page processing
- `flate2` (zlib-rs) — deflate compression for PDF image streams
- `mimalloc` — memory allocator
- `anyhow` — error handling
## Key Design Decisions
### Split (PDF -> Images)
- Each page is fully independent: render + encode + write
- MuPDF Document isn't Send, so each rayon worker opens its own instance
(cheap — just parses xref table; rendering is the real work)
- Semaphore limits in-flight pages to bound memory (300 DPI page = ~26MB RGB)
- Grayscale mode renders via MuPDF's device_gray() directly (faster, 1/3 data)
### Merge (Images -> PDF)
- Phase 1 (parallel): decode/prepare all images via rayon
- Phase 2 (sequential): assemble PDF via lopdf (not Send)
- JPEG: passthrough (DCTDecode, no re-encoding)
- PNG opaque (RGB/Gray/Palette): IDAT passthrough with Predictor 15
- PNG with alpha (RGBA/GrayA): decode, split color+alpha, compress separately, SMask
- Generic formats (TIFF/BMP/GIF): decode via image crate, compress with deflate
## Profiling Reference (47-page lecture PDF, 300 DPI PNG, single-threaded baseline)
| MuPDF rendering (to_pixmap) | 57.8% |
| PNG encoding | 40.0% |
| - png::filter (adaptive) | 20.7% |
| - deflate + adler32 | 14.3% |
| - fdeflate + I/O | 5.0% |
| Other | 2.2% |
## Benchmarks (47-page PDF, release, Apple Silicon M-series)
| PNG 150dpi | 0.21s |
| JPG 150dpi | 0.24s |
| PNG 300dpi | 0.42s |
| JPG 300dpi | 0.56s |