crystal-combine-pdf
= crystal-combine-pdf :toc: macro :toclevels: 2
PDF post-processing in pure Crystal. v0.1 ships page numbering that honours the actual page format (A4, Letter, A3, β¦) and supports partition-aware intra-numbering β the one that lets a music-score booklet show "1/4", "2/4" on a 4-page partition while the global "3/12" stays in the bottom-right corner.
Inspired by the Ruby gem https://github.com/boazsegev/combine_pdf[combine_pdf] β fixes the US Letter hardcoding bug of combine_pdf.number_pages and adds intra-partition numbering, which the upstream gem does not support.
π«π· Lisez ce document en franΓ§ais : link:README.fr.adoc[README.fr.adoc]
toc::[]
== Why
The Ruby gem combine_pdf is the de facto tool for assembling PDF booklets. Its number_pages helper draws the page number at hardcoded coordinates derived from US Letter (612 Γ 792 pt). On A4 (595 Γ 842 pt), Asia A4 or A3 the number lands several centimetres off.
Beyond the format mismatch, an assembled music-score booklet β where each "partition" (sheet) may span 1, 2 or N pages β needs a second layer of numbering: a small "1/4", "2/4", β¦ in the corner of each page of the partition, so the performer knows when to turn the page and how many are left in the current piece.
crystal-combine-pdf does both, in pure Crystal, with the page geometry read from the actual MediaBox of every page.
== Features (v0.1)
- Global page numbering in the bottom-right corner (format customisable, default
"N/T"). - Intra-partition numbering in the top-left corner (format customisable, default
"n/t"), shown only when the partition has more than one page (configurable). - A4-aware, Letter-aware, A3-aware β coordinates computed from each page's
MediaBox, no per-format flag needed. - Skip pages (e.g. covers):
--skip 1,2. - Pure Crystal, single binary, no
pdftk/qpdf/pdftotexton$PATH.
== Roadmap
- v0.2 β multi-PDF merging (the "combine" of
combine_pdf). - v0.3 β booklet assembly: merge + number in one shot (
crystal-combine-pdf assemble file1.pdf file2.pdf β¦). - v0.4 β encryption support (PDF passwords) for ISO 27001 compliance use cases.
- v0.5+ β PAdES-style signatures.
== Installation
Add to your shard.yml:
[source,yaml]
dependencies: crystal-combine-pdf: github: aloli-crystal/crystal-combine-pdf version: "~> 0.1"
then run shards install.
For the CLI:
[source,shell]
git clone https://github.com/aloli-crystal/crystal-combine-pdf cd crystal-combine-pdf shards build --release cp bin/crystal-combine-pdf ~/bin/ # or use crystal-bin-installer
== CLI
[source,shell]
Number every page, format "N/T" in the bottom-right corner.
crystal-combine-pdf number booklet.pdf
Same, with intra-partition marks. Partition sizes are given in
1-based page order β the sum must equal the total page count.
Here: pages 1-4 = partition 1 (so each gets "1/4" through "4/4"),
pages 5-6 = partition 2, page 7 = partition 3 (single-page β no
intra-partition mark by default).
crystal-combine-pdf number booklet.pdf --partitions 4,2,1
Skip the cover (page 1) so it stays untouched.
crystal-combine-pdf number booklet.pdf --skip 1
Custom format and red colour.
crystal-combine-pdf number booklet.pdf
--global-format "Page %page% of %total%"
--partition-format "(%page% / %total%)"
--color "0.7,0.0,0.0"
--font-size 12
=== Options
[cols="1,3"] |=== | Option | Description
| -o FILE, --output FILE | Output path. Defaults to <input>-numbered.pdf next to the input.
| --partitions N,N,β¦ | Comma-separated list of partition sizes, in 1-based page order. Sum must equal the PDF page count.
| --skip PAGES | Comma-separated list of 1-based page indices to leave un-numbered (typical: --skip 1 to spare the cover).
| --font-size SIZE | Point size of the rendered numbers. Default 10.
| --margin PT | Inset from the page edge, in points. Default 24.
| --color R,G,B | RGB triplet, components 0.0β1.0. Default 0.2,0.2,0.2.
| --global-format FMT | Format string for the global page number. Placeholders: %page%, %total%. Default "%page%/%total%".
| --partition-format FMT | Format string for the intra-partition number. Same placeholders. Default "%page%/%total%".
| --show-single-partitions | Render the intra-partition number even when the partition has a single page. Off by default.
| -h, --help, -v, --version | Standard. |===
== API
[source,crystal]
require "crystal-combine-pdf"
CombinePDF.number( input: "booklet.pdf", output: "booklet-numbered.pdf", partitions: [4, 2, 1], options: CombinePDF::Options.new( font_size: 11.0, color: {0.2, 0.2, 0.2}, margin: 24.0, skip_pages: [1], ), )
== Limitations (v0.1)
- No multi-PDF merging yet β feed an already-assembled PDF.
- Numbering is rendered with the standard Type1
Helvetica(no embedded font, no Unicode beyond ASCII digits +/). - The output PDF uses an incremental update (PDF spec Β§ 7.5.6). All real-world PDF readers handle this;
crystal-pdf::Readerv0.3.4+ also does.
== Development
[source,shell]
shards install crystal spec bin/ameba crystal tool format src/ spec/
== License
MIT β see link:LICENSE[LICENSE].
crystal-combine-pdf
- 0
- 0
- 0
- 0
- 2
- about 6 hours ago
- April 25, 2026
MIT License
Sat, 25 Apr 2026 14:54:51 GMT