fotobuch
fotobuch turns a folder of photos into a print-ready PDF photobook — no cropping, no distortion, no manual drag-and-drop.
You point it at your photos, it figures out the layout: aspect ratios stay intact, reading order flows naturally from top-left to bottom-right, and photos from the same event stay together. Fully automatic, or tweak any page by hand.
Quick links
| Where to go | What you’ll find |
|---|---|
| Installation | Binaries, building from source, editor setup |
| Core Concepts | How projects, groups, and layouts work |
| Your First Book | Zero to PDF in 10 minutes |
| Commands | Every command at a glance |
| Printing | Exporting for Saal Digital & friends |
| Full Flag Reference | Every flag, auto-generated from source |
Example project
The repository ships with a complete example in docs/examples/ — a ready-made
project with sample images and both preview and release PDFs. A good way to
explore the YAML, the Typst template, and the output without creating your own
project first.
Source code
Installation
Pre-built binaries (recommended)
Download the latest binary for your platform from the Releases page:
| Platform | File |
|---|---|
| Linux x86_64 | fotobuch-linux-x86_64.tar.gz |
| Windows x86_64 | fotobuch-windows-x86_64.zip |
Extract the archive and place the fotobuch binary somewhere on your PATH.
Verify the install:
fotobuch --version
Build from source
Requirements: Rust (stable) and cmake (needed to build
the HiGHS optimizer library).
git clone https://github.com/EddyXorb/fotobuch.git
cd fotobuch
cargo build --release
# binary: ./target/release/fotobuch
Recommended editor setup
fotobuch writes a Typst source file alongside the PDF.
For a live preview while you work, install
VS Code with the
Typst Preview
extension. Open the .typ file and the preview updates every time you run
fotobuch build.
Alternatively, just keep any PDF viewer open and reload after each build.
Shell completions
fotobuch completions --shell bash >> ~/.bash_completion
fotobuch completions --shell zsh >> ~/.zshrc
fotobuch completions --shell fish > ~/.config/fish/completions/fotobuch.fish
fotobuch completions --shell powershell >> $PROFILE
Core Concepts
Before diving in, here’s how the pieces fit together.
Projects
A fotobuch project is a set of files tracked by Git — a YAML config, a Typst
template, and cached images. When you run fotobuch project new, it creates
a directory with a Git repo inside.
Multiple projects can live in the same Git repo, each on its own branch
(fotobuch/<name>). Switching projects (fotobuch project switch) is a
Git checkout under the hood — your working directory swaps to the other
project’s state.
Photos and groups
When you fotobuch add /some/folder, all photos in that folder become a
group. Groups matter because the solver tries to keep photos from the same
group together on the same page (or on neighbouring pages). Think of a group
as “photos from one occasion”.
Each subfolder you add is a separate group. With --recursive, every subfolder
becomes its own group automatically.
Groups are sorted chronologically — by the date in the folder name if there is one, otherwise by the oldest photo’s timestamp.
Placed vs. unplaced
A photo can be in one of two states:
| State | Meaning |
|---|---|
| unplaced | In the project, but not assigned to any page yet |
| placed | Assigned to a specific slot on a specific page |
When you run fotobuch build for the first time, all photos are placed
automatically — the solver distributes them across pages.
After that first build, any newly added photos start as unplaced. You need
to run fotobuch place to put them into the layout before the next
fotobuch build.
fotobuch status always shows how many photos are unplaced.
Pages and slots
The solver arranges photos into a grid-like layout on each page. Each photo occupies a slot — a rectangular area with a specific position and size.
Both pages and slots are numbered from 0:
Page 0 Page 1 Page 2
┌──┬──┬──┐ ┌─────┬──┐ ┌──┬─────┐
│0 │1 │2 │ │ 0 │1 │ │0 │ │
├──┴──┼──┤ │ ├──┤ ├──┤ 2 │
│ 3 │4 │ ├─────┤2 │ │1 │ │
└─────┴──┘ └─────┴──┘ └──┴─────┘
Slots are ordered left-to-right, top-to-bottom (reading order). Use
fotobuch status <page> to see the slot numbers for a specific page.
A slot address identifies one or more photos:
| Address | Meaning |
|---|---|
3 | All slots on page 3 |
3:2 | Slot 2 on page 3 |
3:2..5 | Slots 2 through 5 on page 3 |
3:2..5,7 | Slots 2–5 and slot 7 on page 3 |
4+ | New page inserted after page 4 (move destination only) |
Two ways to address photos
fotobuch has two different ways to refer to photos, depending on whether they are placed in the layout or not:
By filename or pattern — used for photos that are not yet placed, or when you want to work with photos regardless of their layout position. Matches against the photo’s source path or XMP metadata.
| Command | What it does |
|---|---|
fotobuch add --filter "pattern" | Import only matching photos |
fotobuch add --filter-xmp "pattern" | Import only photos whose XMP metadata matches |
fotobuch place --filter "pattern" | Place only matching unplaced photos |
fotobuch remove "pattern" | Remove matching photos from the project |
By slot address — used for photos that are already placed on a page. Each
placed photo has a unique address like 3:2 (page 3, slot 2). See
Pages and slots below for the full syntax.
| Command | What it does |
|---|---|
fotobuch page move 3:2 to 5 | Move a placed photo to another page |
fotobuch page swap 3:2 5:1 | Swap two placed photos |
fotobuch page weight 3:2 2.0 | Change a placed photo’s weight |
fotobuch page info 3:2 | Show metadata for a placed photo |
fotobuch unplace 3:2 | Remove a placed photo from its slot |
Rule of thumb: use filename patterns when working with unplaced photos
(add, place, remove), and use slot addresses when working with placed
photos (page move, page swap, page weight, unplace).
Weights
Every photo has an area weight (default: 1.0). The solver uses weights to decide how much space each photo gets relative to its neighbours on the same page.
- Weight 2.0 → roughly twice the area of a weight-1.0 photo
- Weight 0.5 → roughly half
Set weights when adding (fotobuch add --weight 3) or per-slot afterward
(fotobuch page weight 2:0 3.0).
Cover
If you create a project with --with-cover, page 0 becomes the cover. The
cover spans the full width (front + spine + back) and has its own bleed and
margin settings. See Known Limitations for current
caveats.
Build pipeline
fotobuch add → photos enter the project (unplaced)
fotobuch place → unplaced photos get assigned to pages
fotobuch build → solver optimizes layout, renders preview PDF
fotobuch build release → renders final PDF at 300 DPI
The first build implicitly places all photos, so you can skip place
on a fresh project.
Every command that changes the layout creates a Git commit automatically.
Use fotobuch undo / fotobuch redo to navigate the history, and
fotobuch history to see the log.
The .fotobuch cache
fotobuch stores resized preview and final images in a .fotobuch/ directory
inside your project. This is purely a cache — you can delete it at any time
without losing data. The next build will regenerate whatever it needs.
Your First Book
This walkthrough takes you from zero to a print-ready PDF.
Prerequisites: fotobuch installed, a folder of photos on your machine.
Prefer learning by example? Check out the complete project in
docs/examples/— it has sample images, YAML config, template, and generated PDFs.
Step 1 — Create a project
fotobuch project new "Italy-2024" --width 297 --height 210
This creates a directory Italy-2024/ with a Git repo, a YAML config, and a
Typst template. The --width and --height values are in millimetres
(297 × 210 mm = A4 landscape).
Switch into the project folder:
cd "Italy-2024"
Project names must start with a letter and can only contain letters, digits, or dashes.
Step 2 — Review the configuration
Before adding photos, open Italy-2024.yaml in a text editor and check the
most important settings. The file already has sensible defaults, but you should
set the page count to match the size of book you want:
config:
book:
page_width_mm: 297.0
page_height_mm: 210.0
bleed_mm: 3.0 # required by most print services
margin_mm: 0.0 # 0 = edge-to-edge; set to e.g. 10 for a white border
gap_mm: 5.0 # the gap between the photos in your layout; if set to 0, photos touch each other
book_layout_solver:
page_target: 20 # how many pages you want
page_max: 24 # upper limit — give the solver some room above the target
The book layout solver is the algorithm that decides how your photos are
distributed across pages. page_target is your desired page count;
page_max is the hard upper limit. Setting page_max a few pages above
page_target gives the solver freedom to use an extra page when that produces
a significantly better layout.
If you plan to use a cover, also set:
cover:
active: true
front_back_width_mm: 594.0 # total width of front + back panel
height_mm: 297.0
spine_text: "Italy 2024" # or ~ for no text
spine_mm: 15.0 # the spine width of your book - does not add to front_back_width_mm, when spine_mode = fixed
spine_mode: fixed # fixed = spine_mm is the exact width; auto = spine width is determined by page count
Other settings you might want to adjust:
search_timeout (solver time limit for large books).
See Configuration for a full reference of all settings.
Tip: You can always change these later and re-run
fotobuch build. The solver will redistribute photos according to the new settings.
Step 3 — Add photos
Point fotobuch at one or more folders. Each folder becomes a group — photos from the same group are kept together on pages, but neighboured groups can mix.
fotobuch add /photos/2024-07-Italy
fotobuch add /photos/2024-08-Hiking
Folders with a date in the name (2024-07-Italy, 20240715_Rome) are sorted
chronologically. Folders without a recognisable date are sorted by the oldest
photo’s timestamp.
You can also add single files, add recursively (each subfolder = its own group), or filter by file name or xmp-data (that is where your rating and other metadata normally sits for each photo, when you use e.g. lightroom):
# single file
fotobuch add /photos/2024-07-Italy/DSC_0042.jpg
# recursive — each subfolder becomes a group
fotobuch add --recursive /photos/2024-summer
# only photos matching a filename pattern
fotobuch add /photos/2024-07-Italy --filter "DSC_00.*\.jpg"
# only 3-to-5-star photos, giving them more space
fotobuch add /photos/2024-07-Italy --filter-xmp "Rating.*[3-5]" --weight 5
Check what was imported:
fotobuch status
Step 4 — Build a preview
fotobuch build
On the first run, fotobuch distributes all photos across pages automatically and
renders a preview PDF at lower DPI (fast). Open Italy-2024.pdf to review the
result — or open Italy-2024.typ in VS Code with
Typst Preview
for a live preview.
Step 5 — Adjust the layout
You will almost certainly want to tweak a few things. The general procedure is to make a change to the layout using the page subcommands move, swap, combine, split.
After applying one of these (or multiple of these if you want), run fotobuch build to
finally apply all the changes to the layout you made.
Move a photo to another page:
fotobuch page move 3:2 to 5
This moves slot 2 on page 3 to page 5. (Pages and slots count from 0 — use
fotobuch status 3 to see which slot is which.)
Swap two photos
fotobuch page swap 2:3 2:7
Swap two pages:
fotobuch page swap 3 7
Split a page into two
fotobuch page split 3:2
Creates a new page after page 3 and moves slot 2 (and all photos after it) to the new page.
Combine two pages
fotobuch page combine 3..7
Moves all photos from pages 4 to 7 to the end of page 3, then deletes pages 4 to 7.
You can also write fotobuch page combine 3,4,5,6,7 for the same effect.
Give a photo more space in the next build (weight > 1 = relatively larger):
fotobuch page weight 3:2 2.0
Re-solve a single page (runs the solver again from scratch for that page):
fotobuch rebuild --page 6
Undo any change:
fotobuch undo
Every change is committed to Git automatically — fotobuch history shows the
log, and fotobuch undo N rolls back N steps.
Rebuild the preview after changes:
fotobuch build
Iterate through the above steps until you are happy with the result.
Step 6 — Adding more photos later
After the first build, newly added photos start as unplaced. Place them before building:
fotobuch add /photos/bonus-shots
fotobuch place
fotobuch build
This will distribute the unplaced photos on matching pages; no new page will be added though. A page is matching for a new photo if the photos timestamp fits into the timestamps of the already existing pages.
You can also place all unplaced photos onto a specific page:
fotobuch place --into 4
or combine it with a filter for filenames:
fotobuch place --filter "DSC_00.*\.jpg" --into 6
Step 7 — Export for print
When you’re happy with the layout:
fotobuch build release
This re-renders all images at 300 DPI and writes Italy-2024_final.pdf.
The file is ready to upload to your print service (e.g. Saal Digital).
See Printing for Saal Digital-specific details.
Configuration
Every project has a {project-name}.yaml file that controls the entire book.
You don’t need to write this file from scratch — fotobuch project new creates
it with sensible defaults. You only edit the parts you want to change.
Run fotobuch config show at any time to see the full resolved configuration
(including all defaults). Use fotobuch config set <key> <value> to change a
value without editing the YAML directly — see the Full Flag Reference
for exact syntax.
For a quick overview of the most important settings to check before your first build, see Step 2 in Your First Book.
Tip: If the solver produces poor results, the most common fixes are: increase
search_timeout(more time), or disableenable_local_search. You may also want to tweak the solver weights according to your needs (e.g. increaseweight_splitif you want groups to be respected more strictly).
Full reference
The YAML has this structure:
config:
book: # page dimensions, margins, bleed, cover
cover: # cover-specific settings (nested inside book)
book_layout_solver: # photo-to-page distribution
page_layout_solver: # single-page layout (genetic algorithm)
weights: # fitness function weights (nested)
preview: # preview rendering options
All fields below are optional unless marked otherwise. Defaults are applied automatically for any field you don’t set.
config.book — Page dimensions and layout
| Field | Default | Description |
|---|---|---|
title | "Untitled" | Book title. Used as the default spine text on the cover. |
page_width_mm | 210.0 | Page width in mm. Set at project creation with --width. For double-page spreads, use the combined width (e.g. 420). |
page_height_mm | 297.0 | Page height in mm. Set at project creation with --height. |
bleed_mm | 3.0 | Bleed area in mm added around each page. Cut off by the printer. Most services require 3 mm. |
margin_mm | 0.0 | Minimum inset from the page edge. 0 = edge-to-edge (photos may bleed). > 0 = white border (bleed extension is disabled). |
gap_mm | 5.0 | Space in mm between photos on the same page. |
bleed_threshold_mm | 3.0 | Only active when margin_mm is 0. If a photo’s edge is closer to the page edge than this value, the layout is scaled so the photo extends fully into the bleed area. Prevents thin white strips at the page edge after cutting. |
dpi | 300.0 | DPI for the final release PDF. Controls the resolution of cached images used in fotobuch build release. |
config.book.cover — Cover page
All cover fields are optional. The cover is inactive by default. Enable it by
setting active: true and providing dimensions.
| Field | Default | Description |
|---|---|---|
active | false | Enable the cover. When true, the first layout entry (page 0) becomes the cover page. |
front_back_width_mm | 0.0 | Total width of front + back panel combined, without the spine. Required when active: true. |
height_mm | 0.0 | Cover height in mm. Required when active: true. |
mode | split | Cover layout mode. Controls how page 0 is solved. split = deterministic solver optimises the cover (default). See Cover modes below. |
spine_clearance_mm | 5.0 | Gap in mm between the photo edge and the spine for front, back, and split modes. Ignored for spread modes. |
spine_text | book title | Text on the spine. Set to ~ (null) for no text. Font size is auto-calculated from the spine width (max 80% of spine width). |
spine_mode | auto | Spine width mode — see below. |
spine_mm_per_10_pages | 1.4 | Auto mode only. Spine thickness per 10 inner pages. Spine width = (inner_pages / 10) * spine_mm_per_10_pages. In auto mode the spine width affects the total cover canvas width that the solver uses. |
spine_width_mm | — | Fixed mode only. A fixed spine width in mm. In fixed mode the spine does not affect the cover canvas width in the solver — it is only used by the template for display and text sizing. |
bleed_mm | 3.0 | Bleed for the cover page (independent from inner-page bleed). |
margin_mm | 0.0 | Margin for the cover page. Same behaviour as the inner-page margin. |
gap_mm | 5.0 | Gap between photos on the cover. |
bleed_threshold_mm | 3.0 | Bleed threshold for the cover. Same behaviour as inner pages. |
Cover modes
When mode is not free, the GA solver is bypassed and slot positions are
calculated deterministically from the cover geometry. A warning is printed if
the number of photos on the cover does not match what the mode expects.
See Cover Modes — Visual Guide for visual examples of each mode.
| Mode | Photos | Behaviour |
|---|---|---|
free | any | GA solver optimises freely (default) |
front | 1 | Photo on the front panel, aspect ratio preserved and centred |
front-full | 1 | Photo fills the entire front panel (may crop) |
back | 1 | Photo on the back panel, aspect ratio preserved and centred |
back-full | 1 | Photo fills the entire back panel (may crop) |
spread | 1 | Photo spans the full spread (over spine), aspect ratio preserved and centred |
spread-full | 1 | Photo fills the full spread (may crop) |
split | 2 | Slot 0 → front, slot 1 → back, aspect ratio preserved and centred |
split-full | 2 | Slot 0 → front, slot 1 → back, each half fully filled (may crop) |
Workflow example — single photo on the front:
# 1. Place the photo onto the cover
fotobuch place cover.jpg --into 0
# 2. Set the mode in the YAML
# cover:
# mode: front
# 3. Rebuild only the cover
fotobuch rebuild --page 0
Workflow example — panorama across full spread:
fotobuch place panorama.jpg --into 0
# cover: { mode: spread-full }
fotobuch rebuild --page 0
Spine modes explained:
-
auto(default): Spine width is calculated from the number of inner pages. The formula isspine_width = (inner_pages / 10) * spine_mm_per_10_pages. The spine width is added tofront_back_width_mmto form the total cover canvas — meaning the solver accounts for the spine. Use this when your print service calculates spine from page count (most common). -
fixed: You providespine_width_mmdirectly. The spine is not added to the canvas width — the solver usesfront_back_width_mmas-is. The fixed spine width is only used by the template for positioning the spine text. Use this when you already know the exact spine width from your print service.
config.book_layout_solver — Photo-to-page distribution
The book layout solver distributes your photos across pages. It first runs a Mixed Integer Program (MIP) to find a globally optimal assignment, then refines it with a local search that evaluates actual layout quality per page.
| Field | Default | Description |
|---|---|---|
page_target | 12 | Target number of pages. The solver tries to hit this count. This is the most important solver setting. |
page_min | 1 | Hard minimum number of pages. |
page_max | 26 | Hard maximum number of pages. Setting this above page_target gives the solver room to add pages when that improves layout quality. |
photos_per_page_min | 1 | Minimum number of photos on any single page. |
photos_per_page_max | 20 | Maximum number of photos on any single page. |
group_max_per_page | 5 | Maximum number of different groups that may share a single page. Lower values keep groups more separated. |
group_min_photos | 1 | When a group is split across two pages, each part must have at least this many photos. Prevents a single “orphan” photo appearing alone on the next page. |
weight_even | 1.0 | MIP objective weight for even photo distribution across pages. Higher = more uniform page fill. |
weight_split | 10.0 | MIP objective weight penalising group splits. Higher = groups are less likely to be split across pages. |
weight_pages | 5.0 | MIP objective weight penalising deviation from page_target. Higher = result stays closer to the target. |
search_timeout | 30s | Time budget for the entire solver (MIP + local search). Increase for large books. YAML format: {secs: 60, nanos: 0}. |
enable_local_search | true | Whether to run the local search after the MIP. The local search shifts page boundaries to improve per-page layout quality. |
mip_rel_gap | 0.01 | Relative optimality gap for the MIP solver (0.0 = exact, 0.01 = accept solutions within 1% of optimal). Tightening this rarely helps and increases solve time. |
max_photos_for_split | 300 | When the total photo count exceeds this, the problem is automatically decomposed into smaller sub-problems solved sequentially. This avoids MIP timeouts on very large books. |
split_group_boundary_slack | 5 | When splitting into sub-problems, the split point may deviate by this many photos from the ideal boundary to prefer splitting at a group boundary. |
max_coverage_cost | 0.95 | (Currently unused — will be removed in a future version.) Was intended as a threshold for the local search to identify “bad” pages, but is not read by the solver. |
config.page_layout_solver — Single-page layout (genetic algorithm)
The page layout solver arranges photos within a single page using a genetic algorithm with island-model parallelism. These are advanced tuning parameters — the defaults work well for most cases.
| Field | Default | Description |
|---|---|---|
seed | 42 | Random seed for the genetic algorithm. Change this to get a different layout for the same input. fotobuch rebuild changes the seed automatically. |
population_size | 750 | Number of individuals (candidate layouts) per island. Larger = better results but slower. |
max_generations | 100 | Maximum number of generations the algorithm runs. |
mutation_rate | 0.3 | Probability that an individual is mutated per generation. |
crossover_rate | 0.7 | Probability that two individuals are recombined per generation. |
elite_count | 20 | Number of best individuals carried over unchanged to the next generation. |
no_improvement_limit | 15 | Stop early if no improvement is found for this many generations. Set to ~ (null) to disable early stopping. |
enforce_order | true | Enforce chronological reading order (top-left to bottom-right) on each page. When true, photos are arranged so earlier photos appear before later ones in natural reading direction. Set to false if you don’t care about photo order and want the solver to optimise purely for visual quality — this often produces tighter layouts. |
islands_nr | CPU cores | Number of independent populations evolved in parallel. Defaults to the number of available CPU cores. |
islands_migration_interval | 5 | Generations between migration events (best individuals are copied between islands). |
islands_nr_migrants | 2 | Number of individuals migrated per island per migration event. |
config.page_layout_solver.weights — Fitness function
The fitness function evaluates how good a single-page layout is. It combines three cost components, each multiplied by its weight. Lower cost = better layout.
| Field | Default | Description |
|---|---|---|
w_coverage | 1.0 | Weight for canvas coverage cost. Penalises unused white space on the page. This is the dominant term. |
w_size | 0.2 | Weight for size distribution cost. Penalises photos that deviate from their target size (determined by their area_weight). |
w_barycenter | 0.0 | Weight for barycenter centering cost. Penalises layouts whose visual centre of mass is far from the page centre. Disabled by default (0.0). |
config.preview — Preview rendering
All preview overlay settings are automatically suppressed in build release.
| Field | Default | Description |
|---|---|---|
show_filenames | false | Show the photo filename as a caption on each photo. Useful for identifying photos when adjusting the layout. |
max_preview_px | 800 | Maximum pixel size (longest edge) of cached preview images. Lower = faster builds, less disk space, blurrier preview. |
show_borders | true | Show red bleed border and blue margin border overlays on each page. |
show_slot_info | true | Show slot address and area weight on each photo (e.g. 3:2 (1.5)). |
config.book.appendix — Photo index
The appendix is a compact photo index appended at the end of both the preview and release PDFs, listing every photo with its group, timestamp, and a page-position reference.
| Field | Default | Description |
|---|---|---|
active | false | Enable the photo index. |
columns | 7 | Number of columns in the listing. |
ref_mode | "positions" | Reference style: "positions" (page.slot, e.g. 2.3) or "counter" (sequential number badge on each photo). |
page_separator | false | Show a page-number header between pages in the listing. |
strip_timestamps | true | Strip leading ISO timestamps from filenames in the listing. |
label_title | "Photo Index" | Title text of the appendix. |
label_page | "Page" | “Page” label used in the cross-reference legend and page separators. |
date_format | "{day}. {month} {year} {hour}:{min}" | Format string for timestamps. Placeholders: {day}, {month}, {year}, {hour}, {min}. |
date_months | ["Jan", …, "Dec"] | Month abbreviations (12 entries, January–December). |
Example: a typical YAML
config:
book:
title: "Italy 2024"
page_width_mm: 420.0 # double-page spread
page_height_mm: 297.0
bleed_mm: 3.0
margin_mm: 0.0
gap_mm: 5.0
cover:
active: true
front_back_width_mm: 594.0
height_mm: 297.0
spine_mode: auto
spine_mm_per_10_pages: 1.4
spine_text: "Italy 2024"
appendix:
active: true
label_title: "Photo Index"
label_page: "Page"
book_layout_solver:
page_target: 20
page_max: 24
photos_per_page_max: 8
search_timeout:
secs: 60
nanos: 0
preview:
show_filenames: true
show_borders: true
show_slot_info: true
max_preview_px: 800
Cover Modes
Each cover mode determines how photos are positioned and sized on the cover. The examples below show the result of each mode with a sample photo.
Mode: front
A single photo on the front panel, with its aspect ratio preserved and centred.
Mode: front-full
A single photo fills the entire front panel (may crop to fit).
Mode: back
A single photo on the back panel, with its aspect ratio preserved and centred.
Mode: back-full
A single photo fills the entire back panel (may crop to fit).
Mode: spread
A single photo spans the full spread (front, spine, and back), with its aspect ratio preserved and centred.
Mode: spread-full
A single photo fills the full spread without cropping space for the spine (may crop the photo).
Mode: split
Two photos: slot 0 goes on the front panel, slot 1 on the back panel. Both have their aspect ratios preserved and are centred.
Mode: split-full
Two photos: slot 0 fills the front panel, slot 1 fills the back panel (each may crop independently).
Mode: free
The genetic algorithm solver optimises photo placement freely without constraints. Use any number of photos.
Command Overview
All commands follow the pattern fotobuch <command> [options].
Run fotobuch --help or fotobuch <command> --help for details,
or see the Full Flag Reference.
Your original photos are never modified. fotobuch only reads your source files to create cached copies at the configured DPI. Commands like
removedelete photos from the project YAML — your originals on disk are untouched.
Commands at a glance
| Command | What it does |
|---|---|
project new | Create a new photobook project |
project list | List all projects in the current repo |
project switch | Switch to another project (checks out its Git branch) |
add | Import photos or folders into the project |
remove | Delete photos from the project entirely |
place | Assign unplaced photos to pages |
unplace | Remove photos from their page slots (they stay in the project) |
build | Solve layout and render preview PDF |
build release | Render final PDF at full resolution (300 DPI) |
rebuild | Re-run the solver on specific pages |
page move | Move photos between pages |
page swap | Swap pages or slots |
page split | Split a page at a slot |
page combine | Merge pages together |
page info | Show photo metadata for slots on a page |
page weight | Set the area weight for one or more slots |
page mode | Toggle a page between auto (solver) and manual placement |
page pos | Move or scale slots on a manual-mode page |
status | Show project overview (or single-page detail) |
config show | Print the resolved configuration with all defaults |
config set | Set a config value using dot-notation (e.g. book.dpi 150) |
history | Show the project change log |
undo | Undo the last N changes |
redo | Redo N undone changes |
For all flags and exact syntax see the Full Flag Reference. For available config keys see Configuration.
remove vs. unplace
removedeletes photos from the project. They are gone (unless youundo).unplacetakes photos off their page but keeps them in the project. They become unplaced and can be re-placed withfotobuch place.
Use remove --keep-files if you want remove-like pattern matching but
unplace-like behaviour (photos stay, just lose their page assignment).
build vs. rebuild
buildrenders the PDF and only re-solves pages that changed since the last build. On the first run it solves everything.rebuild --page Nforces the solver to re-optimize page N from scratch, even if nothing changed. Useful when you’re not happy with a layout.rebuild --allre-solves every page.
Customizing the Template
Every project has a {name}.typ file — a Typst template
that controls how the PDF looks. ^fotobuch generates this file for you, but you
are free (and encouraged) to edit it, if you are proficient with typst.
Tweak it to your needs, use it as a starting point. fotobuch will not overwrite your template once you created it, so your changes stay safe within the project folder and can also be reused in other fotobuch projects.
Make sure that during your edits you do not change the lines
#let is_final = false
#let project_name = "{project_name}"
#let data = yaml(project_name + ".yaml")
#set text(font: "Libertinus Serif")
otherwise it won’t compile.
Important: Always edit
{name}.typ, neverfinal.typ. The final template is auto-generated from yours duringbuild release(withis_final = true). Your changes infinal.typwould be overwritten.
Preview overlays
These settings only affect the preview PDF. They are automatically disabled in
the release build. Configure them in {name}.yaml:
config:
preview:
show_filenames: false # show filename caption on each photo
show_borders: true # red bleed border + blue margin border
show_slot_info: true # slot address and weight on each photo
Turn on show_filenames when you’re trying to identify which photo is where.
Turn off show_slot_info once you’re happy with the layout and just want a
clean preview.
Photo index (appendix)
The template can append a photo index at the end of the book — a compact
reference listing every photo with its group, timestamp, and a reference back to
its page position. Configure it under config.book.appendix in {name}.yaml:
config:
book:
appendix:
active: true
columns: 7
ref_mode: "positions" # or "counter"
label_title: "Photo Index"
label_page: "Page"
| Setting | Default | Effect |
|---|---|---|
active | false | Enable the appendix |
columns | 7 | Number of columns in the listing |
ref_mode | "positions" | How photos are referenced (see below) |
page_separator | false | Show a page-number header between pages |
strip_timestamps | true | Try to strip leading timestamps from filenames |
label_title | "Photo Index" | Localization: Title text |
label_page | "Page" | Localization: “Page” label |
date_format | "{day}. {month} {year} {hour}:{min}" | Timestamp format |
date_months | ["Jan", …, "Dec"] | Month abbreviations |
Reference modes
"positions" (default) — Each photo is referenced as page.slot, e.g.
2.3 means page 2, slot 3. No visual badge is added to the photos.
"counter" — Photos are numbered sequentially (1, 2, 3, …) and a small
badge with the number appears in the bottom-right corner of each photo in the
PDF.
Going further
The template reads layout data from {name}.yaml via #let data = yaml(…).
A few things to keep in mind:
is_finalcontrols preview vs. release mode. Use it to conditionally show or hide elements:#if not is_final [Draft watermark].- Image paths are resolved relative to the project root via
cache_prefix. Preview images live in.fotobuch/cache/{name}/preview/, final images in.fotobuch/cache/{name}/final/.
If you want to start over with the default template, create a fresh project with
fotobuch project new and copy the generated {name}.typ back.
Printing & Export
General checklist
Before uploading your PDF, verify these things:
- Run
fotobuch build release— only the release PDF has full 300 DPI, do NOT UPLOAD THE PREVIEW PDF to be printed, it will be ugly! - Check the terminal output for DPI warnings (photos that are too small for their slot will be listed)
- Open the final PDF (
{name}_final.pdf) and spot-check a few pages
Saal Digital
fotobuch generates PDFs that meet Saal Digital’s technical requirements out of the box:
- Bleed: 3 mm on all sides (configurable via
config.book.bleed_mmin the YAML) - PDF boxes: MediaBox, TrimBox, and BleedBox are set correctly — matching what InDesign would produce
- Resolution: 300 DPI for
build release(configurable viaconfig.book.dpi)
Other print services
Most print-on-demand services accept standard PDF with bleed. Adjust
config.book.bleed_mm if your provider requires a different bleed size.
The default 3 mm works for the majority of European providers.
Known Limitations
There are surely plenty of limitations I am not aware of at the moment, but let’s start with that:
- photo weights are not respected cross-page-wise - will be adressed in future
- No GUI so far for user not used to a cli - will be adressed in future
What fotobuch deliberately does not do
- No pixel-level placement. The solver decides where photos go; you influence it through weights, groups, and rebuild commands.
- No mixed page sizes within one project.
- No image editing (cropping, colour correction, rotation). Prepare your photos beforehand.
Full Flag Reference
This page is auto-generated from the CLI source. Run
cargo run --example generate-cli-docsto regenerate.
Command-Line Help for fotobuch
This document contains the help content for the fotobuch command-line program.
Command Overview:
fotobuch↴fotobuch add↴fotobuch build↴fotobuch build release↴fotobuch rebuild↴fotobuch place↴fotobuch unplace↴fotobuch page↴fotobuch page move↴fotobuch page split↴fotobuch page combine↴fotobuch page swap↴fotobuch page info↴fotobuch page weight↴fotobuch page mode↴fotobuch page pos↴fotobuch remove↴fotobuch status↴fotobuch config↴fotobuch config show↴fotobuch config set↴fotobuch history↴fotobuch undo↴fotobuch redo↴fotobuch project↴fotobuch project new↴fotobuch project list↴fotobuch project switch↴fotobuch init↴fotobuch completions↴
fotobuch
Photobook layout solver and project manager
Usage: fotobuch <COMMAND>
Subcommands:
add— Add photos to the projectbuild— Calculate layout and generate preview or final PDFrebuild— Force re-optimization of pages or page rangesplace— Place unplaced photos into the bookunplace— Remove photos from the layout at a page:slot address (they stay in the project)page— Page manipulation commands (move, split, combine, swap)remove— Remove photos or groups from the bookstatus— Show project statusconfig— Configuration commands (show or mutate)history— Show project change historyundo— Undo the last N commits (default: 1)redo— Redo N previously undone commits (default: 1)project— Project management commandsinit— Create a new photobook project (alias forproject new)completions— Print shell completion script to stdout
fotobuch add
Add photos to the project
Usage: fotobuch add [OPTIONS] [PATHS]...
Arguments:
<PATHS>— Directories or files containing photos to add
Options:
-
--allow-duplicates— Allow adding duplicate photos (by hash) -
--filter-xmp <REGEX>— Only include photos whose XMP metadata matches this regex (can be repeated, all must match) -
--filter <REGEX>— Only include photos whose source path matches this regex pattern (can be repeated, all must match) -
-d,--dry— Preview what would be added without writing anything -
--update— Re-add photos whose path already exists but whose content has changed -
-r,--recursive— Scan directories recursively (each subdir becomes its own group) -
--weight <WEIGHT>— Area weight for all imported photos (default: 1.0)Default value:
1
fotobuch build
Calculate layout and generate preview or final PDF
Usage: fotobuch build [OPTIONS] [COMMAND]
Subcommands:
release— Generate final high-quality PDF at 300 DPI
Options:
--pages <PAGES>— Only rebuild specific pages (0-based, comma-separated or repeated flag)
fotobuch build release
Generate final high-quality PDF at 300 DPI
Usage: fotobuch build release [OPTIONS]
Options:
--force— Force release even if layout has uncommitted changes
fotobuch rebuild
Force re-optimization of pages or page ranges
Usage: fotobuch rebuild [OPTIONS]
Options:
-
--page <PAGE>— Single page to rebuild (0-based index) -
--range-start <RANGE_START>— Start of page range (0-based index, requires –range-end) -
--range-end <RANGE_END>— End of page range (0-based index, inclusive, requires –range-start) -
--flex <FLEX>— Allow page count to vary by +/- N (only with range)Default value:
0 -
--all— Rebuild all pages from scratch
fotobuch place
Place unplaced photos into the book
Usage: fotobuch place [OPTIONS]
Options:
--filter <REGEX>— Only place photos matching this regex pattern (can be repeated, all must match)--into <INTO>— Place all matching photos onto this specific page (0-based index)
fotobuch unplace
Remove photos from the layout at a page:slot address (they stay in the project)
The page is deleted automatically if it becomes empty.
Usage: fotobuch unplace <ADDRESS>
Arguments:
<ADDRESS>— Slot address: “3:2” (slot 2 on page 3), “3:2,7”, “3:2..5”, “3:2..5,7”
fotobuch page
Page manipulation commands (move, split, combine, swap)
Usage: fotobuch page <COMMAND>
Subcommands:
move— Move or unplace photos between pagessplit— Split a page at a slot: photos from that slot onwards move to a new page inserted aftercombine— Merge pages onto the first one, then delete the now-empty source pagesswap— Swap photos between two addresses (only single numbers or ranges, no comma lists)info— Show photo metadata for slots on a pageweight— Set area_weight for one or more slotsmode— Toggle page mode between auto (solver) and manual (user-placed)pos— Reposition or rescale slots on a Manual-mode page
fotobuch page move
Move or unplace photos between pages
Two forms: “SRC to DST” (move) and “SRC out” (unplace).
Addressing: 3 = whole page, 3:2 = slot 2 on page 3, 3:1..3,7 = slots 1-3 and 7, 4+ = new page after 4.
Move examples: “3:2 to 5”, “3,4 to 5”, “3:2 to 4+”. Unplace examples: “3 out”, “3:2 out”.
See the documentation for the full addressing syntax.
Usage: fotobuch page move [ARGS]...
Arguments:
<ARGS>— Expression passed as space-separated tokens, e.g.: 3:2 to 5
fotobuch page split
Split a page at a slot: photos from that slot onwards move to a new page inserted after
Shortcut for page move PAGE:SLOT.. to PAGE+. Error if SLOT is the first slot (would leave the original page empty).
Usage: fotobuch page split <ADDRESS>
Arguments:
<ADDRESS>— Address “PAGE:SLOT”, e.g. “3:4” splits page 3 at slot 4
fotobuch page combine
Merge pages onto the first one, then delete the now-empty source pages
All following page numbers shift down accordingly.
Usage: fotobuch page combine <PAGES>
Arguments:
<PAGES>— Pages expression: “3,5” (page 5 onto 3) or “3..5” (pages 4-5 onto 3)
fotobuch page swap
Swap photos between two addresses (only single numbers or ranges, no comma lists)
Page swap: “3 5” swaps pages, “1..2 5..9” swaps blocks. Slot swap: “3:2 5:6” swaps individual slots, “3:2..4 5:6..9” swaps slot ranges (different sizes ok).
Errors on overlapping ranges or comma-separated lists as operands.
Usage: fotobuch page swap <LEFT> <RIGHT>
Arguments:
<LEFT>— Left address: “3:2”, “3:1..3”, “3”, “3..6”<RIGHT>— Right address: “5:6”, “5:2..4”, “5”, “8..11”
fotobuch page info
Show photo metadata for slots on a page
Address forms: 3 (all slots), 3:2 (single slot), 3:1..3,7 (slots 1–3 and 7).
Without flags: full table (or vertical view for a single slot). With a flag: machine-readable single-field output.
Usage: fotobuch page info [OPTIONS] <ADDRESS>
Arguments:
<ADDRESS>— Address: “3”, “3:2”, “3:1..3,7”
Options:
--weights— Output only area weights (format: page:slot=weight)--ids— Output only photo IDs--pixels— Output only pixel dimensions
fotobuch page weight
Set area_weight for one or more slots
Examples: 3:2 2.0 (single slot), 3:1..3,7 2.0 (multiple slots), 3 2.0 (whole page).
Usage: fotobuch page weight <ADDRESS> <WEIGHT>
Arguments:
<ADDRESS>— Address: “3”, “3:2”, “3:1..3,7”<WEIGHT>— Weight value (must be > 0)
fotobuch page mode
Toggle page mode between auto (solver) and manual (user-placed)
Syntax: fotobuch page mode <pages> <a|m|auto|manual>
Examples: 3 m (page 3 to manual), 3..5 a (pages 3-5 to auto).
Usage: fotobuch page mode <PAGES> <MODE>
Arguments:
<PAGES>— Pages to change: “3”, “3..5”, “3,5”<MODE>— Mode: ‘a’ or ‘auto’ for auto-solver, ‘m’ or ‘manual’ for manual placement
fotobuch page pos
Reposition or rescale slots on a Manual-mode page.
Syntax: fotobuch page pos <address> [--by dx,dy] [--at x,y] [--scale s]
Examples: 4:2 --by -20,30 — move slot 2 on page 4 relatively 4:2 --at 100,50 — set slot 2 origin to (100mm, 50mm) 4:2 --scale 1.5 — scale slot 2 by 1.5× 4:2..5 --by -20,30 — move slots 2–5 together 4:2 --at 100,50 --scale 2 — absolute position + scale
At least one of –by, –at, –scale is required. –by and –at are mutually exclusive. The page must be in manual mode.
Usage: fotobuch page pos <--by <BY>|--at <AT>|--scale <SCALE>> <ADDRESS>
Arguments:
<ADDRESS>— Address: “4:2”, “4:2..5”, “4:1,3”
Options:
--by <BY>— Relative move in mm: “dx,dy” (e.g. “-20,30”)--at <AT>— Absolute position in mm: “x,y” (e.g. “100,50”)--scale <SCALE>— Scale factor applied to width and height (origin stays fixed)
fotobuch remove
Remove photos or groups from the book
Usage: fotobuch remove [OPTIONS] [PATTERNS]...
Arguments:
<PATTERNS>— Photos, group names, or regex patterns to remove (can be repeated)
Options:
--keep-files— Only remove from layout, keep photos in the project (makes them unplaced)--unplaced— Remove all photos that are not placed in any layout page
fotobuch status
Show project status
Usage: fotobuch status [PAGE]
Arguments:
<PAGE>— Show detailed information for a specific page (0-based index)
fotobuch config
Configuration commands (show or mutate)
Usage: fotobuch config <COMMAND>
Subcommands:
show— Show resolved configuration with defaultsset— Set a config value using dot-notation (e.g.book.dpi 300)
fotobuch config show
Show resolved configuration with defaults
Usage: fotobuch config show
fotobuch config set
Set a config value using dot-notation (e.g. book.dpi 300)
Supported keys mirror the YAML config hierarchy. Types are auto-detected: true/false → bool, integers → int, decimals → float, else string.
Usage: fotobuch config set <KEY> <VALUE>
Arguments:
<KEY>— Dot-notation key, e.g. “book.dpi” or “book.cover.active”<VALUE>— New value, e.g. “300”, “true”, “3.5”, “spread”
fotobuch history
Show project change history
Usage: fotobuch history [OPTIONS]
Options:
-
-n <COUNT>— Number of entries to show (0 = all)Default value:
5
fotobuch undo
Undo the last N commits (default: 1)
Usage: fotobuch undo [STEPS]
Arguments:
-
<STEPS>— Number of steps to undoDefault value:
1
fotobuch redo
Redo N previously undone commits (default: 1)
Usage: fotobuch redo [STEPS]
Arguments:
-
<STEPS>— Number of steps to redoDefault value:
1
fotobuch project
Project management commands
Usage: fotobuch project <COMMAND>
Subcommands:
new— Create a new photobook projectlist— List all photobook projectsswitch— Switch to another photobook project
fotobuch project new
Create a new photobook project
Usage: fotobuch project new [OPTIONS] --width <WIDTH> --height <HEIGHT> <NAME>
Arguments:
<NAME>— Project name
Options:
-
--width <WIDTH>— Page width in millimeters -
--height <HEIGHT>— Page height in millimeters -
--bleed <BLEED>— Bleed margin in millimetersDefault value:
3 -
--parent-dir <PARENT_DIR>— Parent directory where project will be created (default: current directory) -
--quiet— Suppress welcome messageDefault value:
false -
--with-cover— Create project with an active cover pageDefault value:
false -
--cover-width <COVER_WIDTH>— Cover width in millimeters (defaults to page_width * 2 if –with-cover is set, with warning) -
--cover-height <COVER_HEIGHT>— Cover height in millimeters (defaults to page_height if –with-cover is set, with warning) -
--spine-grow-per-10-pages-mm <SPINE_GROW_PER_10_PAGES_MM>— Spine width growth per 10 inner pages in mm (auto mode, conflicts with –spine-mm) -
--spine-mm <SPINE_MM>— Fixed spine width in mm (conflicts with –spine-grow-per-10-pages-mm) -
--margin-mm <MARGIN_MM>— Inner margin in millimeters (default: 0)Default value:
0
fotobuch project list
List all photobook projects
Usage: fotobuch project list
fotobuch project switch
Switch to another photobook project
Usage: fotobuch project switch <NAME>
Arguments:
<NAME>— Project name to switch to
fotobuch init
Create a new photobook project (alias for project new)
Usage: fotobuch init [OPTIONS] --width <WIDTH> --height <HEIGHT> <NAME>
Arguments:
<NAME>— Project name
Options:
-
--width <WIDTH>— Page width in millimeters -
--height <HEIGHT>— Page height in millimeters -
--bleed <BLEED>— Bleed margin in millimetersDefault value:
3 -
--parent-dir <PARENT_DIR>— Parent directory where project will be created (default: current directory) -
--quiet— Suppress welcome messageDefault value:
false -
--with-cover— Create project with an active cover pageDefault value:
false -
--cover-width <COVER_WIDTH>— Cover width in millimeters -
--cover-height <COVER_HEIGHT>— Cover height in millimeters -
--spine-grow-per-10-pages-mm <SPINE_GROW_PER_10_PAGES_MM>— Spine width growth per 10 inner pages in mm -
--spine-mm <SPINE_MM>— Fixed spine width in mm -
--margin-mm <MARGIN_MM>— Inner margin in millimeters (default: 0)Default value:
0
fotobuch completions
Print shell completion script to stdout
Usage: fotobuch completions –shell bash >> ~/.bash_completion fotobuch completions –shell zsh >> ~/.zshrc fotobuch completions –shell fish > ~/.config/fish/completions/fotobuch.fish fotobuch completions –shell powershell >> $PROFILE
Usage: fotobuch completions --shell <SHELL>
Options:
-
--shell <SHELL>— Shell to generate completions forPossible values:
bash,elvish,fish,powershell,zsh
This document was generated automatically by
clap-markdown.