Skip to content

PDF command line tool for .NET

Since 1.0View as Markdown

pdfnetstack compresses, edits and protects PDF files from a terminal or a script, with the same engine and the same equivalence proof as the library.

Install the tool once, then run a command per job. optimize is lossless by default and returns the original bytes when a rewrite would not be verified equivalent or would save less than 1%.

Terminal window
dotnet tool install -g PdfNetStack.Tool
pdfnetstack optimize in.pdf out.pdf
pdfnetstack batch ./incoming ./optimized --report report.csv

It needs the .NET 8 or later runtime. pdfnetstack --help prints every command and option.

CommandDoes
optimizeCompresses one file
batchCompresses or measures every PDF in a folder tree
inspectShows where the bytes of a file go
verifyChecks that two files are equivalent
mergeJoins files
splitWrites one file per page
extractCopies a page range into a new file
rotateRotates pages
metaReads or writes the title, author, subject and keywords
encryptAdds password protection
decryptRemoves password protection
ocrMakes scanned pages searchable (separate package, 1.1)
Terminal window
pdfnetstack optimize <input.pdf> <output.pdf> [options]
pdfnetstack optimize in.pdf out.pdf --jpeg-quality 60 # lossy, JPEG images only
pdfnetstack optimize in.pdf out.pdf --max-dpi 150 --to-jpeg --jpeg-quality 75 --min-ssim 0.95
pdfnetstack optimize in.pdf out.pdf --preset balanced # much smaller, no visible change
pdfnetstack optimize in.pdf out.pdf --quantize # drawings and screenshots to a palette
pdfnetstack optimize in.pdf out.pdf --exhaustive # archival: smaller Flate, much slower
pdfnetstack optimize in.pdf out.pdf --linearize # fast web view
pdfnetstack optimize huge.pdf out.pdf --streaming # bounded memory, any input size
pdfnetstack optimize in.pdf out.pdf --json # the full result as JSON

The lossy image options (--max-dpi, --to-jpeg, --quantize, --min-ssim), --preset, --progressive, --exhaustive, --linearize, --streaming and the structural passes with their --no-prune, --no-compact and --no-font-merge switches arrived in 1.1; the 1.0.0 tool does not have them. The reason line names every lossy change.

Prints the disposition, the sizes, the saving and the time, the reason, and for an optimized file the object counts and the time per phase. The output file is always written: the verified rewrite, or the input unchanged. Input and output must be different files. The library equivalent is PdfOptimizer.OptimizeFileAsync.

Terminal window
pdfnetstack batch <input-dir> [output-dir] [options]
pdfnetstack batch ./incoming ./optimized --report report.csv
pdfnetstack batch ./corpus # measure only, writes nothing

Finds every .pdf under the input folder, subfolders included, and writes each result to the same relative path under the output folder. Without an output folder nothing is written: the run measures what the optimizer would save. Documents run in parallel (--jobs, default all cores), each on one thread unless --threads is given.

The summary lists documents optimized, retained and unsupported (grouped by reason), errors, the total size before and after, throughput and peak memory, and any verification failures, for which the original was kept. --report <file.csv> writes one row per document with the columns file, disposition, original_bytes, output_bytes, pages, milliseconds and reason. A value that starts with a formula character is prefixed with a quote, so spreadsheet applications never evaluate it. --json prints the summary as JSON.

Terminal window
pdfnetstack inspect <input.pdf> [--json]

Shows the size, PDF version, page count, objects (and how many are reachable), revisions and linearization, the cross-reference format, whether the file needed repair, is encrypted, signed or PDF/A-1, then the streams grouped by kind with their share of the file, and the filters used. The library equivalent is PdfInspector.Inspect.

Terminal window
pdfnetstack verify <original.pdf> <candidate.pdf>
pdfnetstack verify original.pdf other.pdf --jpeg-quality 60 # accept JPEG requantization
pdfnetstack verify original.pdf other.pdf --min-ssim 0.95 # accept downsampled and converted images (1.1)

Runs the equivalence check the optimizer runs on itself, on the output of any tool. Prints Equivalent. and exits 0, or Different: with the first difference and exits 1. With --jpeg-quality, JPEG images may also differ by exactly the requantization the optimizer applies. With --min-ssim, downsampled and converted images are accepted when they keep their components and aspect, are no larger, keep their mean colors and reach that SSIM against the original resampled to their size; requantized images must then reach it too. See Fidelity and verification.

Terminal window
pdfnetstack merge <output.pdf> <input.pdf>...
pdfnetstack merge out.pdf a.pdf b.pdf c.pdf

The output comes first, then the inputs in order.

Terminal window
pdfnetstack split <input.pdf> <output-dir>

Writes one file per page, named after the input with a zero padded page number, such as report-01.pdf to report-12.pdf.

Terminal window
pdfnetstack extract <input.pdf> <output.pdf> <pages>
pdfnetstack extract in.pdf out.pdf 1-3,7,9-

Pages are numbered from 1. Ranges are separated by commas: 9- runs to the last page, -3 starts at the first, and 5-2 runs backwards. The order given is kept and pages may repeat.

Terminal window
pdfnetstack rotate <input.pdf> <output.pdf> <degrees> [pages]
pdfnetstack rotate in.pdf out.pdf 90 2-4

Rotates clockwise by a multiple of 90 degrees, every page or the given ranges.

Terminal window
pdfnetstack meta <input.pdf> # print
pdfnetstack meta in.pdf out.pdf --title "Annual report" --author "Finance"

Without an output file, prints the page count, every document information entry and the number of top level bookmarks. Text from the document is printed with control and bidirectional override characters escaped, so a crafted title cannot drive the terminal. With an output file, sets any of --title, --author, --subject and --keywords and saves.

Terminal window
pdfnetstack encrypt <input.pdf> <output.pdf> --user-password u --owner-password o
[--allow print,copy,modify,annotate,forms,accessibility,assemble,print-hq] [--aes128]

Writes AES-256 by default, AES-128 with --aes128. At least one password is required; an empty owner password is replaced by a random one. Without --allow every permission is granted; --allow none grants none, and print-hq includes print. See Encryption.

Terminal window
pdfnetstack decrypt <input.pdf> <output.pdf> --password <owner password>

Saves the document without protection. It needs the owner password.

Terminal window
pdfnetstack ocr <input.pdf> <output.pdf> [--languages latin,arabic] [--models <folder>] [options]
pdfnetstack-ocr <input.pdf> <output.pdf> [options] # the same tool, run directly

In the main branch, ships with 1.1. Makes scanned pages searchable: the scan on each page is read and its words are laid over it as invisible text, as the OCR guide describes. OCR needs ONNX Runtime’s native libraries and about 21 MB of models, so it is a tool of its own, pdfnetstack-ocr, in the PdfNetStack.Ocr.Tool package (about 70 MB, models included). pdfnetstack ocr runs it with the same arguments, found next to pdfnetstack or on the PATH, and says how to install it when it is missing:

Terminal window
dotnet tool install -g PdfNetStack.Ocr.Tool
OptionEffectLibrary option
--languages <list>Scripts to read, comma separated: latin, arabic (default latin)Languages
--models <folder>Load the models from this folder instead of the ones installed with the toolOnnxOcrEngine.Load(folder)
--min-confidence <0-1>Leave out lines read with less confidence (default 0.5)MinimumConfidence
--no-orientationDo not look for pages scanned sideways or upside downDetectOrientation = false
--all-pagesAlso read pages that already show textSkipPagesWithText = false
--threads <n>Pages recognized at once (default: all cores)MaxDegreeOfParallelism
--text <file.txt>Also write the text found, pages apart by a blank linePdfOcrReport.Text
--password <password>Open an encrypted input
--no-optimizeSave without the optimizer pass
--jsonPer page report: status, reason, orientation, lines and text

Prints one line per page (lines and words read, how far a sideways page was turned, or why it was skipped) and the output size. The output is written when the run completes, even if some pages failed; it exits 1 in that case. Pages that already show text are left as they are, so a second run changes nothing.

Optimizer options apply to optimize, batch and, where they make sense, to verify.

OptionEffectLibrary option
--fast, --balancedFaster Flate levels (default: maximum)Compression
--exhaustive1.1. Exhaustive Flate search for archival jobs: a few percent smaller, many times slowerCompression = Exhaustive
--no-object-streamsClassic xref table, no object streamsUseObjectStreams = false
--no-dedupDo not merge identical objectsDeduplicateObjects = false
--no-prune1.1. Keep resources no content usesPruneUnusedResources = false
--no-compact1.1. Do not rewrite content streams compactlyCompactContentStreams = false
--no-font-merge1.1. Do not merge subsets of one TrueType fontMergeFontSubsets = false
--linearize1.1. Write a linearized file (fast web view)Linearize
--streaming1.1. Read the input on demand and write the rewrite through a temporary file, proven there; no input size limitStreaming, MaxInputBytes
--no-recompressDo not re-encode streamsRecompressStreams = false
--no-image-predictorsDo not try PNG prediction on raw imagesUseImagePredictors = false
--no-jpegDo not re-encode JPEG imagesOptimizeJpegImages = false
--jpeg-quality <1-100>Requantize JPEG images, and the quality of new JPEGs (lossy)JpegQuality
--max-dpi <n>1.1. Downsample images drawn above 1.5 times n dpi to n (lossy)MaxImageDpi
--max-bilevel-dpi <n>1.2. The same for bilevel images, in place of --max-dpi (lossy)MaxBilevelImageDpi
--to-jpeg1.1. Re-encode photographic raw images as JPEG at --jpeg-quality (lossy)ConvertImagesToJpeg
--quantize1.1. Reduce raw RGB images to a palette when that stays faithful (lossy)QuantizeImages
--preset <balanced|smallest>1.1. A ready set of lossy options; options after it change itPdfOptimizerOptions.Balanced, Smallest
--min-ssim <0-1>1.1. Keep any image whose lossy change falls below this SSIMMinImageSsim
--progressive1.1. Write JPEG progressively when that is smaller (lossless)ProgressiveJpeg
--no-verifySkip the equivalence proof (not recommended)Verify = false
--allow-incrementalFlatten files with incremental updatesAllowIncrementalUpdates
--allow-repairedRewrite damaged files that needed repairAllowRepairedFiles
--strip-thumbnailsRemove page thumbnails (not lossless)RemovePageThumbnails
--strip-pieceinfoRemove application private data (not lossless)RemovePieceInfo
--min-saving <percent>Minimum saving to keep a rewrite (default 1)MinSavingPercent
--threads <n>Threads per document (default: all cores)MaxDegreeOfParallelism
--jobs <n>Documents in parallel for batch (default: all cores)
--report <file.csv>batch: one row per document
--jsonMachine readable output

The editing commands (merge, split, extract, rotate, meta, encrypt, decrypt) save through the lossless optimizer; --no-optimize skips that pass, and --fast or --balanced set the Flate level. Every editing command accepts --password to open an encrypted input.

The output of pdfnetstack --help, captured from the tool built from the same source as this page:

pdfnetstack: lossless PDF optimizer
Usage:
pdfnetstack optimize <input.pdf> <output.pdf> [options]
pdfnetstack batch <input-dir> [output-dir] [options] (no output-dir: measure only)
pdfnetstack inspect <input.pdf> [--json]
pdfnetstack verify <original.pdf> <candidate.pdf>
Editing:
pdfnetstack merge <output.pdf> <input.pdf>...
pdfnetstack split <input.pdf> <output-dir>
pdfnetstack extract <input.pdf> <output.pdf> <pages> e.g. 1-3,7,9- (order kept, repeats allowed)
pdfnetstack rotate <input.pdf> <output.pdf> <degrees> [pages]
pdfnetstack meta <input.pdf> [output.pdf] [--title t] [--author a] [--subject s] [--keywords k]
Security:
pdfnetstack encrypt <input.pdf> <output.pdf> --user-password u --owner-password o
[--allow print,copy,modify,annotate,forms,accessibility,assemble,print-hq] [--aes128]
pdfnetstack decrypt <input.pdf> <output.pdf> --password <owner password>
Every editing command accepts --password to open an encrypted input.
OCR (needs the PdfNetStack.Ocr.Tool package; pdfnetstack-ocr --help for its options):
pdfnetstack ocr <input.pdf> <output.pdf> [--languages latin,arabic] [--models <folder>]
Options:
--fast | --balanced Faster Flate levels (default: maximum)
--exhaustive Exhaustive Flate search for archival jobs: a few
percent smaller, many times slower
--no-object-streams Write a classic xref table, no object streams
--no-dedup Do not merge identical objects
--no-prune Keep resources no content uses
--no-compact Do not rewrite content streams compactly
--no-font-merge Do not merge subsets of one TrueType font
--linearize Write a linearized file (fast web view)
--streaming Read the input on demand and write the rewrite
through a temporary file, proven there: memory
stays bounded for files larger than memory, and
inputs of any size are accepted
--no-recompress Do not re-encode streams
--no-group4 Do not re-encode bilevel images as CCITT Group 4
--no-image-predictors Do not try PNG prediction on raw images
--no-jpeg Do not re-encode JPEG images
--jpeg-quality <1-100> Requantize JPEG images to this quality (lossy);
verify: accept images requantized this way
--max-dpi <n> Downsample images drawn above 1.5 times n dpi to n (lossy)
--max-bilevel-dpi <n> The same for bilevel images, in place of --max-dpi (lossy)
--to-jpeg Re-encode photographic raw and JPEG 2000 images as
JPEG at --jpeg-quality (lossy)
--quantize Reduce raw RGB images to a palette when that stays
faithful: drawings and screenshots (lossy)
--preset <name> balanced: palette, JPEG 75, SSIM at least 0.95.
smallest: palette, JPEG 50, 150 dpi, SSIM at least
0.90. Both lossy; options after it change it
--min-ssim <0-1> Perceptual gate: keep any image whose lossy change
falls below this SSIM; verify: accept downsampled
and converted images that meet it
--progressive Write JPEG progressively when that is smaller
--no-verify Skip the equivalence proof (not recommended)
--allow-incremental Flatten files with incremental updates
--allow-repaired Rewrite damaged files that needed repair
--strip-thumbnails Remove page thumbnails (not lossless)
--strip-pieceinfo Remove application private data (not lossless)
--min-saving <percent> Minimum saving to keep a rewrite (default 1)
--threads <n> Threads per document (default: all cores)
--jobs <n> Documents processed in parallel by batch (default: all cores)
--report <file.csv> Batch: write one row per document
--json Machine readable output
--no-optimize Editing: save without the optimizer pass
CodeMeaning
0Success. For verify: the files are equivalent
1verify: the files differ. ocr: the output was written, but some pages failed
2Wrong arguments, an unreadable PDF, a wrong password, or a file error. The message goes to standard error

An optimize or batch run that keeps the original, because the saving is too small or the file is refused by the safety model, is a success: the output is a valid PDF.

  • The tool works on files only; it has no stdin or stdout mode.
  • Lossy options handle 8 bit gray, RGB and CMYK images, raw or JPEG; --to-jpeg needs --jpeg-quality. JPEG 2000, JBIG2, CCITT, 16 bit, indexed, Lab, Separation and DeviceN images keep their resolution.
  • By default the optimizer holds each document in memory while it processes it. For large image heavy files, lower --threads or --jobs, or use --streaming, which keeps memory bounded and needs room for the output in the temporary folder next to it. See Memory and performance.

Questions

How do I install the pdfnetstack command?
Run dotnet tool install -g PdfNetStack.Tool. It needs the .NET 8 or later runtime and works on Windows, Linux and macOS.
Can pdfnetstack compress a whole folder of PDFs?
Yes. pdfnetstack batch <input-dir> <output-dir> optimizes every PDF under the folder, subfolders included, and --report writes one CSV row per document. Without an output folder it only measures.