Skip to content

Latest commit

 

History

29 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PDF Toolkit — bmd · bcrop · bzip · bbox

Four tools that turn Markdown or loose PDFs into a finished, bordered, page-numbered book.

  Markdown ──→ bmd ──┐
                     ├──→ bzip ──→ bbox ──→ finished PDF
  PDFs ──────────────┘      ↑ crops on the way through

Nothing in the chain re-renders your content. Vector text stays vector, scanned images come out byte-identical, and images are only touched when you ask for a size limit.

One command

bflowmd notes/          # markdown  → render, merge, crop, contents, border
bflowpdf scans/         # PDFs      → merge, crop, contents, border

bflowmd takes markdown; bflowpdf takes PDFs. Pointing bflowpdf at a folder of .md is refused with a message rather than doing nothing useful. Either accepts a single file or a folder. Everything lands in out_pdfs/ beside the sources, and every path produced is printed at the end. Missing Mermaid or KaTeX assets are fetched automatically.

Both run with defaults, so the bare command already does the useful thing:

  --crop  --numbering per-file  --toc-file  --toc-depth 3

You get the book, a contents page and the labels file. Turn any of it off, or override it:

  --no-crop               skip the crop pass
  --no-toc                skip the contents page
  --no-border             stop before bbox
  --numbering continuous  one sequence across the whole book
  --toc-depth N           how deep the contents goes

Anything you pass wins over the matching default, and any other bzip option is handed straight through.

Layout

pdf-toolkit/
  bflowmd  bflowpdf        one-command workflows
  bmd/     bmd.py    bmd.yaml
  bcrop/   bcrop.py  bcropui.py  bcrop.yaml
  bzip/    bzip.py   bzip.yaml
  bbox/    bbox.py   config.yaml   bbox-labels.patch
  docs/    bmd.md  bcrop.md  bcropui.md  bzip.md
Tool What it does Docs
bmd Markdown → PDF: Mermaid, math, syntax highlighting docs/bmd.md
bcrop Detects and removes dead margins, losslessly docs/bcrop.md
bcropui Review window: facing pages, redaction boxes docs/bcropui.md
bzip Merges, numbers, bookmarks, contents, size control docs/bzip.md
bbox Your border tool, patched for --labels bbox/bbox-labels.patch

bbox/config.yaml is yours — the package does not ship one, so your settings are never overwritten. Copy your existing file in.

Installation

pip install -r requirements.txt

# only if you will render Markdown
playwright install chromium
python bmd/bmd.py --fetch-assets   # shared asset copy, once

# only if you will use the crop review window
#   Debian/Ubuntu: sudo apt install python3-tk
#   macOS/Windows: tkinter ships with Python

Each tool reads the config in its own folder, so nothing collides.

The venv can live beside pdf-toolkit/ or one level up in a parent project folder — bflowmd and bflowpdf locate themselves and then walk up looking for .venv/, so either layout works without configuration.

The two workflows

Markdown in. No cropping needed for its own sake, because bmd sets the page geometry itself — the crop step only exists to undo a browser's margins. The flows still crop by default, which trims bmd's render margin so BBox owns all the spacing.

bflowmd notes/

or step by step, if you want to inspect between stages:

bmd notes/                                   # → notes/out_pdfs/*.pdf
bzip notes/out_pdfs --crop --numbering per-file --toc-file --toc-depth 3
bbox <the file bzip named> --labels <the labels file bzip named>

Each tool prints the exact next command, so you never have to construct those paths yourself.

PDFs in. Crop first, because you did not control the source margins.

bflowpdf chapters/

Cropping through bzip --crop runs the detection on the merged book, so one box covers every page and the whole thing comes out one size. Cropping each file separately gives each its own box and leaves you with mismatched pages.

To review the box by eye, or to erase something a crop cannot reach:

bcrop scan.pdf --ui

Spacing

The distance from page edge to text is cumulative:

   final space = bmd --margin  +  bbox outer_margin  +  bbox inner_padding

With bmd's default 0.25in and BBox's usual 0.1 + 0.1in, text sits 0.45in from the edge. Use bmd --margin 0 to reproduce the tighter look of a Briss-cropped page and let BBox own all the spacing.

Note that outer_margin does not move the page number. Only page_numbers.margin does, measured from the page edge: 20 puts the number level with the first line of content, 10 lifts it into the corner.

Page numbering

bzip decides which number goes on which page; bbox still owns how it looks. That split is why --labels exists instead of new formatting options — your format string, font and position config keeps working untouched.

Every label entry carries two scopes:

placeholder meaning
{n} / {total} position within the source document
{page} / {pages} position across the whole book

With format: "{n}/{total} · **{page}**/{pages}" a page prints 1/9 · 10/24. Any part of the format wrapped in **stars** is drawn bold — here it marks the number the contents page lists.

This needs --numbering per-file. Under continuous numbering the two pairs are the same number by definition and you get 10/24 · 10/24. BBox says so when it spots that combination.

--skip-first N leaves front matter unnumbered. bzip also writes real PDF /PageLabels, so the viewer's page field agrees with the printed number.

Output names

Generated PDFs go into an out_pdfs/ folder beside the sources, so your markdown and originals never get buried in output:

Command Produces
bmd notes/ notes/out_pdfs/*.pdf
bmd notes/one.md notes/out_pdfs/one.pdf
bzip notes/out_pdfs notes/out_pdfs/notes_combined.pdf + notes_combined_labels.json
bzip notes/out_pdfs --crop notes/out_pdfs/notes_crop_combined.pdf
bzip … --toc-file also notes_crop_combined_toc.pdf
bcrop doc.pdf <doc's folder>/out_pdfs/doc_crop.pdf
bcrop pdfs/ pdfs/out_pdfs/*_crop.pdf
bbox doc.pdf doc_bbox.pdf, beside the input

bbox is the exception — it writes next to its input rather than into out_pdfs/, because by then you are already working inside that folder.

If the inputs are already inside out_pdfs/, the result lands there rather than nesting another level, and is named after the parent folder. A second bzip run on the same folder ignores the merged PDFs the first run produced instead of merging them back in; bcrop reprocesses everything by default, since cropping an already-cropped file is a no-op.

--outdir, -o and --labels override any of this. The folder name is output_dir in bmd.yaml, bcrop.yaml and bzip.yaml.

Contents page

bzip chapters/ --crop --toc-file

writes ..._toc.pdf beside the book. Entries come from the bookmark tree, so --toc-depth 1 lists documents only and 3 adds two levels of their headings. Titles come from each document's own H1, falling back to its metadata title and then the filename.

The contents page is never numbered, so content pages stay 1…N and its listed numbers stay correct whether you keep it separate, print it, or merge it in later:

bzip 00_toc.pdf book.pdf -o book_with_toc.pdf --skip-first 1

--toc inserts it directly instead, which also makes the entries clickable — a standalone file cannot link across documents reliably.

The labels file

bzip writes it, bbox reads it. It is the map of which page number goes on which page, and it is what makes --numbering per-file work.

You do not have to name it: bzip writes it beside the merged PDF automatically and prints the exact bbox command, complete with both paths. --labels PATH puts it somewhere else, --no-labels skips it.

Page numbers
bbox book.pdf One continuous run, 1 to N, computed by bbox
bbox book.pdf --labels … Whatever the map says

The map is only interesting when the numbering is not a plain sequence — a restart at each document, or unnumbered front matter. For plain continuous numbering the two are identical and you can ignore labels entirely.

Everything else — font, position, format, border — always comes from config.yaml. The map only supplies which number goes on which page.

The review window

bcrop scan.pdf --ui
bcrop scan.pdf --ui --parity      # facing pages, side by side

The default view is the darkest-pixel union of every page, so anything inside the box is inside it everywhere. Each edge names the page that set it, with a button to jump there. Preview bbox result reads bbox/config.yaml and shows the finished page.

With --parity both halves appear side by side. Mirrored is the default: same size, gutters on opposite sides, which is what a bound book needs. Identical copies margins literally and will clip every facing page on a gutter book.

Draw box marks an area to erase from every page in the group — for a page number that sits inside the content box, where no rectangle can crop it away without taking a strip of text with it. This is the one thing BCrop does that is not lossless.

Running unattended

Every tool that asks for confirmation takes -y. To skip prompts everywhere at once, set the environment variable instead:

export PDFKIT_YES=1

bmd -y also accepts its asset download prompt, so a first run on a machine with no assets/ folder fetches them and carries on rather than stopping.

Changes to bbox.py

Four, all additive. bbox-labels.patch has the exact diff.

  1. --labels support, including {page}/{pages} and **bold** runs in the format string. Without the flag, the page-number code runs exactly the original arithmetic.
  2. Bookmarks, page labels and metadata now survive. BBox composes a new PDF, so anything attached to the document rather than a page was being dropped silently — which would have made bzip's bookmarks pointless.
  3. A pre-existing pypdf incompatibility, fixed. compress_identical_objects(remove_use_as=True) throws TypeError on pypdf 5.x and breaks --quality standard entirely.
  4. --border-color fixed. It rescaled the colour to 0–255 and handed that to PyMuPDF, which wants 0–1, so every colour except black raised an error.

Nothing about borders, spacing, fonts, positioning or the ASCII preview was touched.

One setting worth changing on your side: set quality: original in your config.yaml. high rasterizes every page at 300 DPI — measured on a real book, 106KB became 875KB and 5,327 characters of selectable text became 15. original is correct for both vector and scanned sources.

Config file names

Tool Reads
bbox.py bbox/config.yaml
bcrop.py bcrop/bcrop.yaml
bzip.py bzip/bzip.yaml
bmd.py bmd/bmd.yaml

config.yaml is accepted as a fallback, but only if it contains a section that tool recognises — otherwise it warns and ignores the file. Without that check, dropping BBox's config into a shared folder silently handed the other tools BBox's output_suffix: "_bbox".

The review window reads bbox/config.yaml deliberately, since matching BBox is the point of the preview.

Quick reference

# Everything at once
bflowmd notes/                             # md → finished book
bflowpdf scans/                            # pdf → finished book
bflowmd notes/ --toc-depth 2 --no-border

# Markdown
bmd notes/                                 # one PDF per file → out_pdfs/
bmd notes/ --single -o book.pdf --toc      # one combined PDF
bmd --check                                # verify renderer and assets

# Cropping
bcrop input.pdf                            # → out_pdfs/input_crop.pdf
bcrop input.pdf --dry-run                  # show the box, write nothing
bcrop input.pdf --ui                       # review window
bcrop scan.pdf --parity --ink-threshold 235 --noise-pixels 6

# Merging
bzip chapters/ --crop --numbering per-file --toc-file
bzip --manifest order.txt -o book.pdf      # explicit order and titles
bzip scans/ --max-size 10                  # cap the output size

# Borders
bbox book.pdf --labels book_labels.json

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages