imposer 0.3.0

A Rust library and CLI for imposing single-sheet PDFs into booklet layouts with configurable n-up binding.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# 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
```