CLI
dyndyn-pdf is a single self-contained executable (or a global .NET tool) that converts PDF
documents to pixel-faithful, live-text HTML from the command line — no server, no browser, no
external services.
dyndyn-pdf convert report.pdf -o out/
That produces out/report.html: one file, fonts/images/SVG inlined as data URIs, text you can
select and search. See library.md for the same conversion driven from code, and
server.md for the hosted HTTP alternative.
Install
As a .NET global tool (needs the .NET 10 runtime):
dotnet tool install -g DynDyn.Pdf.Cli
dyndyn-pdf convert report.pdf -o out/
The tool package bundles the managed engine plus the native pdfium binaries for every supported
platform under runtimes/<rid>/native/, so one install works everywhere the .NET 10 runtime does.
As a self-contained, single-file binary (no .NET runtime needed on the host):
dotnet publish src/DynDyn.Pdf.Cli -c Release -r linux-x64 --self-contained \
-p:PublishSingleFile=true -o ./publish/linux-x64
Supported RIDs (the native rendering library is auto-restored from NuGet for each; no manual install step): win-x64, win-arm64, win-x86, linux-x64, linux-arm64 (incl. musl/Alpine), osx-x64, osx-arm64.
A build published with -p:NativeAssets=false drops the native payload (and pdfium/SkiaSharp)
entirely — smaller binary, fully managed, --engine native and --background raster then report
exit code 4 instead of being available. See managed-vs-native.md for
when that trade-off makes sense.
Commands
convert — convert one or more PDFs
dyndyn-pdf convert <inputs...> [options]
<inputs...> accepts, in any combination, one or more of:
- A file path — taken as-is, whatever its extension.
- A directory — every top-level
*.pdfinside it. - A glob —
*.pdf, orreports/**/*.pdffor recursive matching.
There is no stdin/stdout mode: every input is a real path on disk, and every output is written to
disk (never printed to stdout). Matches across all inputs are de-duplicated and sorted
(ordinal, case-insensitive) for a deterministic batch order. Anything that resolves to zero PDFs
(a typo, a missing file) is reported as warning: no PDF matched '<pattern>' on stderr and does
not fail the run by itself — the run only fails with exit code 3 if no input resolved to
anything.
Batch mode engages automatically whenever more than one PDF resolves — no separate flag needed. Each document gets its own converter, reader, and font registry (full isolation); one document failing does not stop the rest, and their outputs are still written.
# One document
dyndyn-pdf convert report.pdf -o out/
# A whole folder, 8 documents at a time, per-page output
dyndyn-pdf convert reports/ -o out/ --format pages --jobs 8
# A recursive glob, selected pages, viewer app packaging
dyndyn-pdf convert "reports/**/*.pdf" -o out/ --format viewer --pages 1-20
Options
| Flag | ConversionOptions |
Default | Values / range | Notes |
|---|---|---|---|---|
-o, --output <dir> |
OutputDirectory |
input's directory | any writable dir | |
-f, --format <v> |
(writer choice) | single |
single, pages, viewer |
single=one file; pages=folder+assets; viewer=single-file app w/ TOC + zoom |
-e, --engine <v> |
(converter choice) | managed |
managed, native |
managed=clip-aware, real Bézier fonts; native=pdfium |
--raster-fallback <v> |
RasterFallback |
auto |
never, auto, always |
Substitute a native raster background where the vector one can't composite; text stays live. Never trips exit 4 — it degrades to the vector background with a warning instead |
--semantic-tags |
SemanticStructure |
off | flag | Emit <h1-6>/<p>/<ul>/<li>/<table>… from a tagged PDF via display:contents. No effect on untagged PDFs |
--detect-decorative |
DetectDecorative |
off | flag | Detect running headers/footers and page numbers as non-selectable decorative text |
--extract-styles |
ExtractStyles |
off | flag | Dedup repeated style declarations into short CSS classes; off = byte-identical output |
--dpi <n> |
Dpi |
120 |
18–600 | CSS pixel scale = dpi/72 |
-p, --pages <range> |
Pages |
all | 1-5,8,10- |
1-based; applies to every document in a batch |
--line-grouping <v> |
LineGrouping |
always |
always, auto, never |
always=merge into justified lines; auto=merge only where pixel-accurate; never=every word at its exact position |
--no-line-grouping |
LineGrouping = Never |
off | flag | Alias for --line-grouping never |
--background <v> |
Background |
svg |
svg, raster, none |
raster needs the native rendering library and SkiaSharp |
--no-background |
Background = None |
off | flag | Alias for --background none |
--jpeg-quality <n> |
JpegQuality |
85 |
1–100 | Opaque background images only (alpha images use PNG) |
--parallelism <n> |
MaxDegreeOfParallelism |
all cores | ≥1 |
Managed post-processing threads within one document; in batch mode, derived from --jobs when unset |
-j, --jobs <n> |
BatchOptions.MaxConcurrentDocuments |
derived from cores + available memory | ≥1 |
PDFs converted concurrently; ignored for a single input |
--raw-ttf |
FontEmbedding = RawTtf |
off (Woff1) |
flag | Raw TrueType instead of WOFF 1.0; larger output, a font-debugging aid |
--password <string> |
Password |
none | string | User password for an encrypted PDF. Managed engine only — passing it with --engine native prints a warning and is ignored |
-v, --verbosity <level> |
(log level) | normal |
quiet, minimal, normal, detailed, diagnostic |
See table below |
--report <path> |
(CLI-only) | none | file path | Writes a machine-readable JSON report: per-document input hash/timing/exit status, per-page counts (spans, paths, images, fonts, bytes), and every warning as {page, code, severity, message}. Written regardless of the exit code, including on failure/cancellation |
--fail-on <v> |
(CLI-only exit-code gate) | none |
none, loss, any |
Exit code 5 when the conversion completes but degrades fidelity. loss=any Loss-severity warning; any=any warning at all. Per document in batch mode |
--timeout <seconds> |
(CLI-only) | 0 (disabled) |
≥0 |
Hard per-document timeout. On expiry the document is abandoned via cancellation and reported with exit code 7; a batch document timing out does not abort the rest |
-v/--verbosity maps to a log level:
| Level | Log level |
|---|---|
quiet |
none |
minimal |
warning |
normal |
information |
detailed |
debug |
diagnostic |
trace |
A live progress line is shown when verbosity ≤ normal and stdout is a TTY (not redirected).
The full option reference — including library-only settings with no CLI flag — lives in configuration.md.
info — inspect a document without converting
dyndyn-pdf info report.pdf
Prints file size, page count, and page geometry (grouping consecutive pages that share the same
size/rotation). Reads only the page-tree dictionaries — it never decodes content streams or loads
fonts, so it stays fast even on content-heavy documents. Exit 3 if the file doesn't exist, 1 on
a parse failure.
render — native-only rendering oracle (build-conditional)
Present only in builds that include the native payload (i.e. not -p:NativeAssets=false
publishes). It calls pdfium directly rather than through the managed pipeline and exists as
internal fidelity-benchmark tooling, not part of the conversion story described here.
Exit codes
Script against these — they are a stable contract:
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Conversion failed (or, in batch, at least one document failed — successes are still written) |
2 |
Invalid arguments (bad --dpi, --jpeg-quality, --pages range, --timeout, or an output-path collision in a batch) |
3 |
Input not found (no PDF matched any input) |
4 |
A native-only feature (--engine native or --background raster) was requested but the native rendering library (or, for raster, SkiaSharp) is unavailable for this RID. --raster-fallback never triggers this — it degrades to the vector background with a warning instead |
5 |
The conversion completed and produced output, but --fail-on (default none, so this never fires unless requested) was tripped. In batch mode, a hard document failure (1) takes priority over a 5 when both occur across the batch |
6 |
The PDF is password-protected and no password (or the wrong one) was supplied via --password |
7 |
The conversion was aborted by --timeout |
8 |
The PDF is encrypted with a security handler this engine does not implement (e.g. /Adobe.PubSec public-key/certificate encryption) — no password can ever unlock it |
Full defaults and the library/REST equivalents for every flag are in configuration.md. For containerizing the CLI as a batch job, see deployment.md.