# imposer
A small, focused Rust library and CLI for arranging PDF pages into booklet layouts (n-up) with common binding options.
## Features
- **Flexible n-up Imposition**: Support for power-of-two pages-per-sheet (2-up, 4-up, 8-up, 16-up, 32-up, ...)
- **Multiple Binding Types**: Saddle stitch and perfect binding with full mathematical accuracy
- **Automatic Padding**: Intelligently pads to maintain proper sheet divisibility
- **Correct Page Ordering**: Proper duplex booklet page ordering for accurate printing
## Binding Types Explained
### Saddle Stitch (Nested Binding)
Saddle stitch is the traditional booklet binding where pages are nested together. Each sheet wraps the previous one, creating a nested structure.
**Structure:**
```
Sheet 1 (outer): Front: [Last, First] Back: [Second, Last-1]
Sheet 2 (inner): Front: [Last-2, Third] Back: [Fourth, Last-3]
Sheet 3 (inner): Front: [Last-4, Fifth] Back: [Sixth, Last-5]
... (continues inward)
```
**Example: 8 pages, 2-up saddle stitch:**
```
Sheet 1: Front: [8, 1] Back: [2, 7]
Sheet 2: Front: [6, 3] Back: [4, 5]
↓ Nest sheets (Sheet 2 inside Sheet 1)
Result: Pages fold correctly from outside to inside
```
### Perfect Binding (Signatures Stacking)
Perfect binding groups pages into **signatures**, where each signature internally uses saddle-stitch ordering, but signatures are then **stacked sequentially** (not nested). This is how paperback books are bound.
**Structure:**
```
Signature 1: Pages 1-8 (internally: saddle-stitch ordered)
Signature 2: Pages 9-16 (internally: saddle-stitch ordered)
Signature 3: Pages 17-24 (internally: saddle-stitch ordered)
↓ Stack signatures (Sig 2 behind Sig 1, etc.)
```
## Key Concept: Sheets Per Signature
The `sheets_per_signature` parameter controls how many physical sheets go into each signature:
- **1 sheet per signature**: Each signature contains a single nested sheet. The entire booklet is one large stacking of signatures. For 8 pages with 2-up and 1 sheet/sig, you get 2 signatures of 4 pages each.
- **2+ sheets per signature**: Multiple sheets are nested within each signature before stacking. For 8 pages with 2-up and 2 sheets/sig, you get 1 signature where both sheets are nested (identical to pure saddle stitch).
## Important: Physical Sheets vs. Printing Sheets
The `sheets_per_signature` parameter refers to **physical sheets after cutting**. Different n-up values produce different numbers of physical sheets from one printing sheet:
- **2-up**: 1 printing sheet → 1 physical sheet
- **4-up**: 1 printing sheet → 2 physical sheets (cut horizontally)
- **8-up**: 1 printing sheet → 4 physical sheets (cut horizontally and vertically)
This means `--perfect-binding --sheets-per-signature 2 --2up` produces the **same page grouping** as `--perfect-binding --sheets-per-signature 4 --4up`, just with different grid layouts.
## Example: 16 pages perfect binding comparison
```
Saddle Stitch (single signature):
Sheet 1: [16, 1, 14, 3] / [2, 15, 4, 13]
Sheet 2: [12, 5, 10, 7] / [6, 11, 8, 9]
(all nested together)
Perfect Binding with 1 sheet/sig:
Signature 1 (pages 1-8):
Sheet 1: [8, 1, 6, 3] / [2, 7, 4, 5]
Signature 2 (pages 9-16):
Sheet 1: [16, 9, 14, 11] / [10, 15, 12, 13]
(signatures stacked)
Perfect Binding with 2 sheets/sig:
Signature 1 (pages 1-16):
Sheet 1: [16, 1, 14, 3] / [2, 15, 4, 13]
Sheet 2: [12, 5, 10, 7] / [6, 11, 8, 9]
(identical to pure saddle stitch)
```
## Algorithm Implementation
The imposition algorithms correctly:
- Pad odd-page inputs to the next multiple of `pages_per_sheet`
- Calculate optimal grid layout for page arrangement
- Apply proper page reversal for duplex printing (varies by n-up value)
- Handle arbitrary power-of-2 n-up values
- Preserve blank pages in mathematically correct positions for proper folding
- Verified with 109-page test across multiple n-up values (all pages present, no duplicates)
## Documentation
Find the crate and detailed documentation online:
- [crates.io: imposer](https://crates.io/crates/imposer)
- [docs.rs: imposer](https://docs.rs/imposer)
## Notes and Capabilities
- Assumes reasonably uniform page sizes across input PDF
- Pads output with blank pages for proper sheet divisibility
- Blank pages appear in mathematically correct positions (not consolidated)
## Command-line options reference
### Required options
- **`-i, --input <PATH>`**: Input PDF file path. Must be a valid PDF.
- **`-o, --output <PATH>`**: Output PDF file path. The generated booklet will be written here.
### Layout and binding options
- **`-n, --pages-per-sheet <N>`** (default: 2): Number of pages to arrange per sheet side. Must be a power of 2 for saddle stitch (2, 4, 8, 16, 32, 64, ...). No restriction for perfect binding.
- **`-p, --page-size <SIZE>`** (default: a4): Output page size. Valid options: `a4`, `a3`, `a5`, `letter`, `legal`, `tabloid`.
- **`--perfect-binding`**: Use perfect binding instead of saddle stitch. Perfect binding stacks signatures sequentially (like a paperback book) rather than nesting them. Cannot be used with saddle-stitch-specific settings.
### Perfect binding signature control
**Note**: These options only apply when `--perfect-binding` is used.
- **`--sheets-per-signature <N>`** (default: 1): Number of physical sheets to nest within each signature. A value of 1 creates simple stacking; higher values create sewn signatures with nested sheets inside.
- **`--num-signatures <N>`** (optional): If specified, pages are evenly distributed across this many signatures. This takes precedence over `--sheets-per-signature`. Useful when you want a specific number of signature sections regardless of content.
**Important note on `--sheets-per-signature` and `--num-signatures`**: These are mutually exclusive concepts:
- Use `--sheets-per-signature` when you want to control the physical structure of each signature (how many sheets are nested per signature).
- Use `--num-signatures` when you want to control the number of signature sections the document is divided into. If both are specified, `--num-signatures` takes precedence.
### Scaling and appearance options
- **`--no-scale-to-fit`**: Do not scale source pages to fit the output page size. By default, pages are scaled to fill the available space while respecting margins.
- **`--no-preserve-aspect-ratio`**: Allow pages to be stretched or compressed to fill the output page. By default, aspect ratio is preserved, which may result in letterboxing or pillarboxing.
- **`--draw-guides`**: Draw fold and cut guide lines on the output. Useful for debugging and visualizing the page layout before printing.
- **`--number-pages`**: Replace page content with page numbers. Useful for visualizing the page order without needing to view the actual PDF content.
### Default behavior
If you run `imposer -i input.pdf -o output.pdf` with no other options:
- Uses **saddle stitch** binding
- Creates a **2-up** layout (2 pages per sheet)
- Output size is **A4**
- Pages are **scaled to fit** the output size while **preserving aspect ratio**
- No guides or page numbers are drawn
## Building
```bash
cargo build --release
```
## Development
This project uses [cargo-make](https://github.com/sagiegurari/cargo-make) to replicate CI checks locally.
### Install cargo-make
```bash
cargo install cargo-make
```
### Run all CI checks
```bash
cargo make
```
This runs all checks that CI performs:
- Code formatting (`cargo fmt --check`)
- Clippy linting (`cargo clippy`)
- Rust tests (`cargo nextest run`)
- Python bindings build and tests
- Python bindings synchronization check
### Available tasks
- `cargo make` - Run all CI checks (default)
- `cargo make quick` - Quick checks (format + clippy only)
- `cargo make rust-only` - Rust checks only (skip Python)
- `cargo make all-continue` - Run all checks, continue on errors (useful for debugging)
- `cargo make fmt` - Auto-format code
- `cargo make clippy-fix` - Auto-fix clippy issues
- `cargo make test` - Run Rust tests only
- `cargo make python-test` - Run Python tests only
- `cargo make --list-all-steps` - List all available tasks
**Note:** The default task stops on first error (matching CI behavior). If you want to run all checks even when some fail, use `cargo make all-continue`.
### Pre-commit workflow
Before committing, ensure all checks pass:
```bash
cargo make
```
If `cargo make` passes locally, CI should also pass.
## Try it
```bash
cargo run --bin imposer -- -i input.pdf -o booklet.pdf
```
## Installation
Using the binary (when Cargo bin is in your PATH)
If you prefer to run `imposer` directly from your shell without `cargo run`, install the binary into your Cargo bin directory (usually `~/.cargo/bin`) and make sure that directory is on your PATH.
- Install locally from the repository (installs into `~/.cargo/bin`):
```bash
cargo install --path .
```
- Or install the published crate from crates.io:
```bash
cargo install imposer
```
- Ensure `~/.cargo/bin` is on your PATH (add to your shell profile if necessary):
```bash
export PATH="$HOME/.cargo/bin:$PATH"
# Add the line above to ~/.bashrc, ~/.zshrc, or your shell profile to make it persistent
```
## Using imposer from the command line
Once `imposer` is installed and in your PATH, you can run it directly:
```bash
imposer -i input.pdf -o output.pdf
```
### Common usage examples
**Saddle stitch 2-up (default):**
```bash
imposer -i input.pdf -o booklet.pdf
```
**Saddle stitch 4-up (4 pages per sheet):**
```bash
imposer -i input.pdf -o booklet.pdf -n 4
```
**Perfect binding with 2 sheets per signature, 2-up layout:**
```bash
imposer -i input.pdf -o book.pdf --perfect-binding --sheets-per-signature 2
```
**Perfect binding 4-up (larger pages per sheet):**
```bash
imposer -i input.pdf -o book.pdf --perfect-binding -n 4
```
All options and defaults are available via the help command:
```bash
imposer --help
```
## Python bindings
`imposer` also provides Python bindings via PyO3, allowing you to use the library directly in Python code. This is useful for:
- Integrating PDF imposition into Python applications
- Batch processing PDFs programmatically
- Embedding booklet generation in web services
- Automating workflows in tools like Scribus
### Installation
Install the Python package from PyPI:
```bash
pip install imposer
```
Or install locally from the repository:
```bash
pip install .
```
### Basic usage
```python
import imposer
# Create a default 2-up A4 saddle-stitch booklet
config = imposer.BookletConfig()
imposer.generate_booklet_from_file("input.pdf", "output.pdf", config)
```
### Configuration options
```python
import imposer
# Configure a custom booklet
binding = imposer.BindingType.perfect_bound(
sheets_per_signature=8, # 8 sheets per signature
num_signatures=3 # 3 total signatures
)
config = (
imposer.BookletConfig()
.with_page_size(imposer.PageSize.a4())
.with_pages_per_sheet(4)
.with_binding_type(binding)
.with_draw_guides(True) # Draw cut/fold lines
.with_number_pages(True) # Add page numbers
.with_scale_to_fit(True) # Scale pages to fit
.with_preserve_aspect_ratio(True)
)
imposer.generate_booklet_from_file("input.pdf", "output.pdf", config)
```
### In-memory processing
Process PDFs without intermediate files:
```python
import imposer
# Read input PDF
with open("input.pdf", "rb") as f:
input_bytes = f.read()
# Generate booklet configuration
config = imposer.BookletConfig()
# Process in memory
output_bytes = imposer.generate_booklet(input_bytes, config)
# Write output
with open("output.pdf", "wb") as f:
f.write(output_bytes)
```
### Available page sizes
```python
imposer.PageSize.a4() # A4 (210 × 297 mm)
imposer.PageSize.a3() # A3 (297 × 420 mm)
imposer.PageSize.a5() # A5 (148 × 210 mm)
imposer.PageSize.letter() # Letter (8.5 × 11 in)
imposer.PageSize.legal() # Legal (8.5 × 14 in)
imposer.PageSize.tabloid() # Tabloid (11 × 17 in)
```
### Binding types
```python
# Saddle-stitch (default): pages nested, stapled in the middle
binding = imposer.BindingType.saddle_stitch()
# Perfect binding: signatures stacked, glued at spine
binding = imposer.BindingType.perfect_bound(
sheets_per_signature=4, # Pages per signature (default: 1)
num_signatures=None # Override with specific signature count
)
```
### Examples
See `examples/python_example.py` for detailed examples including:
- Basic booklet creation
- Different page sizes
- Various n-up layouts
- Perfect binding with signatures
- Guides and page numbers
- In-memory processing
- Scribus integration
Run the example:
```bash
python examples/python_example.py input.pdf
```