ovid 0.1.3

fast, bidirectional PDF and Image converter
# 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)
├── install.sh              # curl | sh installer script
├── .github/workflows/
│   └── 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)

| Phase                        | % of time |
|------------------------------|-----------|
| 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)

| Config     | Time  |
|------------|-------|
| PNG 150dpi | 0.21s |
| JPG 150dpi | 0.24s |
| PNG 300dpi | 0.42s |
| JPG 300dpi | 0.56s |