Page-level PDF manipulation for RustCFML
— merge, extract, delete, rotate and split — delivered as a
.rcx extension
you install into a stock engine. Backed by lopdf,
in-process: no child process, no temp directory, no pdfcpu or qpdf binary
in the container.
// A job pack: the cover the app rendered, then whatever the customer attached.
pdfMerge( [ coverPdf, expandPath( "/uploads/drawing.pdf" ), termsPdf ], expandPath( "/out/pack.pdf" ) );
// Split an upload into one file per page for review.
pages = pdfSplit( uploadPath, expandPath( "/review/page-{page}.pdf" ) );
// Bytes in, bytes out — nothing here needs to touch the disk.
firstPage = pdfExtractPages( fileReadBinary( path ), 1 );Full reference: docs/pdf-api.md.
RustCFML has no JVM, so <cfpdf> — which was always PDFBox underneath — is
not supported, and its built-in Pdf* family (pdfRead, pdfInfo,
pdfPageCount, pdfToImage) is deliberately read-only. Applications that
merged a cover onto attachments or split a scanned upload were left shelling
out to pdfcpu: a Go binary to install, a process to spawn per call, and a
temp file to write and read back around every operation.
This extension is the writing half, kept small on purpose. It does the
structural operations — the ones that move pages between documents — and
nothing that draws. Rendering stays with the engine (pdfToImage); producing
new documents stays with the Typst extension.
Three tools, three seams.
| Function | Does |
|---|---|
pdfMerge( sources [, dest] ) |
concatenate, in order, keeping each page's size/rotation/resources |
pdfExtractPages( src, pages [, dest] ) |
keep only these pages |
pdfDeletePages( src, pages [, dest] ) |
drop these pages |
pdfRotatePages( src, pages, degrees [, dest] ) |
rotate by a multiple of 90° |
pdfSplit( src [, dest pattern] ) |
one document per page |
pdfCountPages( src ) |
page count from a path or bytes |
pdfExtensionVersion() |
which build is loaded |
Two conventions run through all of them:
- A source is a path or a Binary, interchangeably, and an array for
pdfMergemay mix them. - A destination is optional. Omit it and you get a Binary back; give one and the file is written and its path returned. A caller holding bytes never touches the filesystem; a caller holding files never reads them.
pages is <cfpdf>'s grammar — "1-3,5,8-", 1-based, inclusive — or a
number, or an array of either. An out-of-range page is refused by number,
not ignored.
Take the .rcx for your platform from the
latest release
— one of pdf-<version>-{macos-aarch64,linux-x86_64,linux-aarch64,windows-x86_64}.rcx:
rustcfml ext install pdf-0.1.0-linux-x86_64.rcx --user
rustcfml ext list…or drop it into your application's extensions/ directory and check it in.
Requires RustCFML ≥ v0.635.0 (the extension ABI). It is ~530 KB.
To build it yourself, this crate needs a RustCFML checkout beside it —
rustcfml-module is a path dependency until the wrapper is published:
some-dir/
RustCFML/ # at v0.635.2 or later
RustCFML-Extension-Pdf/ # this repo
rustcfml ext build . # produces pdf-0.1.0.rcx for THIS platform
rustcfml ext install pdf-0.1.0.rcx --usercargo test # 14 unit tests over lopdf, no engine needed
rustcfml ext build .
rustcfml ext install pdf-0.1.0.rcx --dir examples/selftest/extensions
rustcfml examples/selftest/selftest.cfm # expect "all checks passed"The CFML suite builds its own fixtures (a hand-written multi-page PDF, so
nothing that can write a PDF is needed) and checks every output with the
engine's own, independent parser — hayro, behind pdfRead/pdfInfo. So
a merged page keeping its inherited size, or a rotated page coming out
landscape, is verified by a second implementation, not by lopdf reading back
what lopdf wrote.
- Bookmarks, outlines, form fields and named destinations do not survive a
merge. They refer to pages by object id across documents; carrying them
means merging the trees and re-targeting every link.
pdfcpu mergedrops them too. Not yet built — say if you need it. - Encrypted input opens if the user password is empty and is written back unencrypted. A real password is refused up front rather than producing garbage.
- Nothing is drawn. No watermark, stamp, header, form fill or thumbnail.
Those are either the engine's (
pdfToImage) or Typst's. - Positional arguments only. BIFs declared by an extension do not carry
parameter names, so
pdfMerge( sources = … )does not bind.
This extension's own code is dual-licensed MIT OR Apache-2.0 — see LICENSE-MIT, LICENSE-APACHE and COPYRIGHT.
A built .rcx statically links lopdf and its dependencies, so it carries their
notice obligations. THIRD-PARTY.txt reproduces every one,
is generated from the resolved dependency graph (./scripts/gen-licenses.sh),
and is packaged inside the archive together with NOTICE and the
licence files.