Skip to content

About

PDF manipulation for RustCFML — merge, extract, delete, rotate and split pages — as a native .rcx extension backed by lopdf

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

RustCFML Extension: PDF

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.

Why

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.

What it provides

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 pdfMerge may 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.

Installing it

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 --user

Verifying it

cargo 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.

Limits, stated

  • 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 merge drops 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.

Licence

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.

About

PDF manipulation for RustCFML — merge, extract, delete, rotate and split pages — as a native .rcx extension backed by lopdf

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages