Skip to main content

spotpdf

CI Python 3.11+ MIT License

spotpdf is a small, fail-closed command-line tool for finding PDF spot colors, renaming spot plates, changing their composite CMYK preview, converting a strictly supported named /Separation to explicit DeviceCMYK paint, and removing supported vector or text spot paint. Pages stay vector-based; the tool does not rasterize them.

[!IMPORTANT] This is a focused beta, not a general PDF preflight engine. Unsupported spot uses stop the whole operation without publishing a partial output.

Before and after

These screenshots use a fully synthetic PDF generated by examples/create_demo_pdf.py. No customer or production files are included in this repository.

Before: process artwork plus three spots After: remove --all
Synthetic vector PDF before processing, with a blue process-color block, pink varnish fill, pink cut contour, and green personalization text Synthetic vector PDF after spot removal; the blue process-color block remains while varnish, cut contour, and personalization text are gone

Convert one spot plate to explicit process paint

convert removes one supported spot plate and replaces every one of its tints with explicit DeviceCMYK vector or text paint. The recipe is supplied by the operator; it is never inferred from the old alternate preview. This synthetic demo deliberately supplies the equivalent 0,62,0,0 recipe, so the composite render remains pixel-identical while the Varnish plate disappears.

Terminal walkthrough showing the Varnish spot plate converted to explicit DeviceCMYK while unrelated spots remain and the equivalent composite render stays pixel-identical

Rename without changing the artwork

rename changes the plate alias, not its color recipe or paint values. The same synthetic demo is rendered before and after this command; the two PNGs must be pixel-identical.

Terminal walkthrough showing Varnish renamed to Varnish Renamed while all other spot plates stay unchanged and the render remains pixel-identical

Change the composite preview without changing the plate

set-alternate keeps the spot name, tint operands, and vector content intact. Only the /Separation fallback used for composite display changes. In this synthetic example, Varnish is remapped from magenta to cyan with --cmyk 100,0,0,0.

Original alternate preview After: set-alternate
Synthetic PDF before the Varnish alternate preview is changed The same synthetic PDF after only the Varnish alternate preview is changed from magenta to cyan

Install

Python 3.11 or newer is required. Install the latest stable release as an isolated command with uv:

uv tool install spotpdf==0.7.0
spotpdf --version

Or use pipx:

pipx install spotpdf==0.7.0

Stable v0.7.0 contains list, check, remove, rename, set-alternate, and the fail-closed convert command, plus verified mutation dry runs and the typed package-root Python API. It also includes the single-pass inventory, configurable processing budgets, and versioned JSON automation contract documented below.

For development, clone the repository and use the locked environment:

git clone https://github.com/Hyperrick/spotpdf.git
cd spotpdf
uv sync --locked --dev
uv run spotpdf --help

Quick start

List every reachable named colorant from /Separation, /DeviceN/NChannel, and page /SeparationInfo declarations:

spotpdf list input.pdf

Default text output from the synthetic demo:

NAME             ROLE  KIND         PAGES  PAINT OPS  STATUS
CutContour       spot  Separation   1      2          painted
Personalization  spot  Separation   1      1          painted
Varnish          spot  Separation   1      1          painted

JSON automation

Every input-processing command can emit exactly one schema-versioned JSON object. Text remains the default:

spotpdf --format json list input.pdf
{"command":"list","exit_code":0,"ok":true,"result":{"colorant_count":1,"colorants":[{"contexts":["painted"],"kinds":["Separation"],"name":"Varnish","pages":[1],"paint_operations":1,"roles":["spot"]}],"input":"input.pdf"},"schema_version":"spotpdf.cli/v1","spotpdf_version":"0.7.0"}

Successful results go to stdout. Runtime errors and parser errors after a valid --format json selection produce one JSON object on stderr. See JSON output and automation for the complete result fields, stable error codes, Enfocus Switch setup, CI examples, and compatibility rules.

ROLE distinguishes spot, process, all, and none. For NChannel color spaces, arbitrary names listed in /Process /Components are process channels, not spots. With a CMYK NChannel process space, canonical /Cyan, /Magenta, /Yellow, and /Black components also remain process channels even when they are omitted from /Process /Components; this shortcut does not apply to RGB or Lab process spaces.

Check one exact, case-sensitive name:

spotpdf check input.pdf --spot "Varnish"

list shows the complete named-colorant inventory, including process channels. check --spot answers only for spot/removal candidates, so a process-only NChannel component shown by list is reported as absent by check.

Semantic inventory work is single-pass: removal hazards are attributed in one resource scan and every reached page or compatible Form stream is interpreted once, not once per declared colorant. Shared Form paint is counted once while every compatible calling page is reported. A separate budget preflight traverses the graph, measures decoded content, and streams operator tokens without building another instruction list. See the reproducible 64/128-spot benchmark.

Rename one spot plate without changing its preview color or paint values:

spotpdf rename input.pdf --spot "Old Name" --to "New Name" -o output.pdf

Rename matching is exact and case-sensitive. The command changes every supported semantic reference to the source plate as one transaction, including role-aware DeviceN/NChannel spot participation, matching /Colorants and /MixingHints entries, page /SeparationInfo, and normal printer-mark appearances. It does not alter alternate color spaces, tint transforms, tint operands, resource aliases, or content streams. This is plate aliasing, not a conversion to process color. The source must have at least one reachable true /Separation definition; DeviceN-only sources are rejected, while consistent DeviceN/NChannel participation is renamed alongside that Separation. See PDF compatibility for the deliberately unsupported prepress structures and the relevant sections of the official Adobe-hosted ISO 32000-1:2008 copy. The initial supported subset fails closed when the source occurs only as an extra /MixingHints colorant for a DeviceN that neither declares it as a component nor supplies a matching /Colorants Separation.

Change only one spot plate's composite CMYK preview:

spotpdf set-alternate input.pdf --spot "Varnish" \
  --cmyk 0,80,100,0 -o output.pdf

The four values are finite percentages from 0 through 100. Every reachable matching /Separation gets the same linear DeviceCMYK fallback; the spot name, plate identity, resource aliases, content streams, and paint operands remain unchanged. This is a composite-preview update, not conversion to process CMYK. The initial supported subset rejects any target-related DeviceN/NChannel use instead of guessing at multi-ink semantics. Values are stored with PDF number precision, so values extremely close to an endpoint can round to 0 or 100. The default text output and JSON result report the values actually stored.

Convert one supported spot plate to an explicit process recipe:

spotpdf convert input.pdf --spot "Varnish" \
  --to-cmyk 0,80,100,0 -o output.pdf

For each selected Separation tint t, convert writes explicit DeviceCMYK t × (C,M,Y,K). Selecting the color space without a following tint is treated as tint 1, as required by the PDF color-state model. The command preserves geometry, clipping, and supported text positioning, removes only the target's now-unused resource aliases and Separation definitions, and leaves unrelated spots intact. It never derives the requested recipe from the existing alternate color or performs ICC color conversion. DeviceN, target-bearing images, patterns, shadings, Type 3 fonts, transparency, effective overprint, annotation appearances, and context-dependent Forms fail closed. Conversion also refuses a planned stream that doubles as both page and Form content or as non-content data such as an attachment or metadata. A changed /Contents Array is consolidated only when none of its member streams has another owner. See PDF compatibility for the exact initial subset.

The same operation is available as a library API:

from pathlib import Path

from spotpdf import convert_spot_to_cmyk

result = convert_spot_to_cmyk(
    Path("input.pdf"),
    Path("output.pdf"),
    "Varnish",
    (0, 80, 100, 0),
)
print(result.page_content_sequences_changed, result.pages_affected)

See the Python API guide for the canonical root imports, typed result records, controlled error hierarchy, and compatibility policy.

Remove supported paint for one name:

spotpdf remove input.pdf --spot "Varnish" -o output.pdf

Remove all supported named spots in one atomic rewrite:

spotpdf remove input.pdf --all -o output.pdf

--all preserves NChannel process components and, conservatively, the exact canonical names /Cyan, /Magenta, /Yellow, and /Black, plus the reserved names /All and /None. Matching is case-sensitive: a custom /black is removed by --all. An exact request such as --spot Black still removes that exact Separation.

Existing outputs are protected unless --force is supplied:

spotpdf remove input.pdf --all -o output.pdf --force

Even with --force, the old output is replaced only after the new PDF has been written to a temporary file, reopened, parsed, and checked for remaining target spots, stale rename references, or an incorrect alternate definition. The same atomic publication guarantee applies to rename, set-alternate, and convert.

Verify a mutation without publishing it

Every mutating command requires exactly one of -o/--output or --dry-run. A dry run is not an inventory-only estimate: spotpdf performs the same budget checks, safety scan, mutation, serialization, strict reopen, and semantic verification as a normal output, but uses private temporary storage and deletes the verified PDF before reporting success:

spotpdf remove input.pdf --all --dry-run
spotpdf rename input.pdf --spot "Old Name" --to "New Name" --dry-run
spotpdf set-alternate input.pdf --spot "Varnish" \
  --cmyk 0,80,100,0 --dry-run
spotpdf convert input.pdf --spot "Varnish" \
  --to-cmyk 0,80,100,0 --dry-run

Successful text output says Dry run and no output published. With --format json, the normal command result is returned with "dry_run": true and without an output field. Processing failures keep the usual exit codes and error records, and no requested output path or directory is created. --force is unnecessary and has no effect with --dry-run.

Processing budgets

These controls are included in stable v0.7.0.

Every input-processing subcommand applies finite per-input ceilings before analysis or mutation:

Resource CLI option Default
Input file --max-input-bytes 805,306,368 bytes (768 MiB)
Pages --max-pages 10,000
Reachable graph entries --max-reachable-objects 1,000,000
Decoded page/Form content --max-decoded-content-bytes 268,435,456 bytes (256 MiB)
Content operators --max-operators 5,000,000

The boundary is inclusive. Raise an individual positive-integer value only for a trusted large job:

spotpdf remove large.pdf --all -o clean.pdf \
  --max-input-bytes 1073741824 --max-pages 20000

An overrun exits without publishing output; --force still preserves an existing destination. These application ceilings are deterministic refusal points, not CPU/RAM quotas or a sandbox. Exact counting semantics, library configuration, and limitations are in the processing-budget guide and security policy.

Exit codes

Code Meaning
0 The command completed; for check, the name is absent.
1 Processing, validation, budget enforcement, or status-stream I/O failed.
2 check found the requested name. The query itself completed successfully.
64 Command-line arguments or option values were invalid.

Because “present” is an intentional check result, shell scripts should handle exit code 2 explicitly. JSON does not change exit semantics: a present result has ok: true, exit_code: 2, and result.present: true. In v0.6.0 and later, argument errors use the unambiguous exit code 64.

The “no output published” guarantee applies to PDF processing failures. If a caller closes stdout or stderr while a successful mutation is reporting its status, the already verified PDF can exist even though the status transport returns an I/O failure; see the JSON stream contract for handling guidance.

For causes and safe next actions for common expected failures, see the troubleshooting guide.

Supported scope

PDF construct list / check rename set-alternate convert remove
Named /Separation declarations Yes Yes Yes, preview slots only Yes, exact target only Yes, for the supported paint below
Vector fills, strokes, and combined path paint Yes Unchanged Unchanged Yes, explicit k/K color state Yes
Text fill and stroke paint Yes Unchanged Unchanged Yes, supported opaque non-Type3 paint Yes, when positioning remains safe
Nested Form XObjects Yes Definitions only Definitions only Yes, with consistent caller context Yes, with context and nesting safeguards
/DeviceN and NChannel declarations Role-aware Consistent spot components alongside a true Separation No; target occurrence fails closed No; target occurrence fails closed No; selected spot components fail closed
/Colorants and /MixingHints dependencies Yes Yes, when structurally consistent Names unchanged No; target dependency fails closed No; selected dependencies fail closed
Page /SeparationInfo Yes Yes, when structurally consistent Names unchanged No; target occurrence fails closed No; selected pre-separated colorants fail closed
PrinterMark normal appearances (/AP /N) Yes Yes, when structurally consistent Definitions only No; target occurrence fails closed No
TrapNet /SeparationColorNames Exact dependency inventory No; target occurrence fails closed Names unchanged No; target occurrence fails closed No
Type 5 halftones, OPI, PrinterMark /AP /R or /AP /D Target-aware hazard status No; target occurrence fails closed Definitions only No; target occurrence fails closed No; target occurrence fails closed
Images, patterns, and shadings Inventory only Definitions only Reachable definitions only No No
Inline images Resource declarations plus unsupported status Definitions only Resource aliases work; embedded target definitions fail closed Target-colored: no; unrelated only in unchanged streams No
Type 3 fonts and soft masks Inventory only Definitions only Reachable definitions only No No
Signed PDFs Read-only inspection No; rewriting invalidates signatures No; rewriting invalidates signatures No; rewriting invalidates signatures No; rewriting invalidates signatures
Encrypted or modification-restricted PDFs Limited by parser No No No No
Parser-detectable syntax errors or warnings Limited by parser No No No No

Removal also stops for clipping text, mixed text that would require font metrics, shared Forms that need conflicting caller state, unresolved resources, cyclic Forms, and Form nesting beyond the documented safety limit. See the compatibility notes for exact behavior.

If a requested removal spot is absent, a normal run copies the input byte-for-byte to the new output path. A dry run verifies the same private copy and discards it. A missing rename, set-alternate, or convert source is an error and publishes no output. The tool never edits an input file in place.

Reproduce the demo

Regenerate all five documentation images from the current CLI and a fresh synthetic PDF:

uv run python scripts/create_docs_images.py

The script verifies that the rename and equivalent-conversion renders are pixel-identical and that the alternate preview render changes before replacing the checked-in images. To inspect the mutation steps manually:

mkdir -p tmp/pdfs/demo
uv run python examples/create_demo_pdf.py tmp/pdfs/demo/input.pdf
uv run spotpdf list tmp/pdfs/demo/input.pdf
uv run spotpdf set-alternate tmp/pdfs/demo/input.pdf --spot Varnish \
  --cmyk 100,0,0,0 -o tmp/pdfs/demo/alternate.pdf
uv run spotpdf convert tmp/pdfs/demo/input.pdf --spot Varnish \
  --to-cmyk 0,62,0,0 -o tmp/pdfs/demo/converted.pdf
uv run spotpdf remove tmp/pdfs/demo/input.pdf --all \
  -o tmp/pdfs/demo/output.pdf
uv run spotpdf list tmp/pdfs/demo/output.pdf

With the default text format, the final command should print No reachable named colorants found. To render PNGs like the README images, install Poppler and use pdftoppm:

mkdir -p tmp/images
pdftoppm -png -r 144 -singlefile \
  tmp/pdfs/demo/input.pdf tmp/images/before
pdftoppm -png -r 144 -singlefile \
  tmp/pdfs/demo/alternate.pdf tmp/images/alternate
pdftoppm -png -r 144 -singlefile \
  tmp/pdfs/demo/output.pdf tmp/images/after

Mutation safety model

  1. Check input bytes, open without parser recovery, and reject open-time warnings.
  2. Enforce page, graph, decoded-content, and operator budgets; then run qpdf's full syntax check and reject final warnings.
  3. Inventory reachable color declarations and exact-name dependencies.
  4. Build a complete rename, alternate-preview, or conversion plan, or perform a complete internal removal planning pass before changing the in-memory document.
  5. Apply only the planned slots or selected paint changes as one operation.
  6. Save beside the destination, reopen strictly, and verify the result.
  7. Atomically replace the destination only after every check succeeds.

This intentionally favors a clear refusal over a PDF that merely looks correct in one viewer. See architecture for module boundaries and the mutation pipeline. Publication checks and processing budgets reduce risk; they do not make the process a sandbox.

Development

uv sync --locked --dev --group release
uv lock --check
uv run ruff check .
uv run ruff format --check .
uv run python scripts/check_python_size.py
uv run python -m unittest discover -s tests -v
artifact_dir="$(mktemp -d)"
uv build --no-build-isolation --out-dir "$artifact_dir"
uv run --no-sync python scripts/check_pypi_readme.py \
  "$artifact_dir"/*.whl "$artifact_dir"/*.tar.gz
uv run --no-sync twine check --strict \
  "$artifact_dir"/*.whl "$artifact_dir"/*.tar.gz
uv run python scripts/check_distribution.py "$artifact_dir"
uv run python scripts/smoke_distributions.py "$artifact_dir"

The distribution check rejects private fixture material and also requires the canonical Support and Security project links in both the wheel and source archive metadata.

All PDF test fixtures are generated at runtime. Do not add confidential, customer, or production PDFs. Contribution and fixture rules are in CONTRIBUTING.md. Releases additionally run the pinned public prepress corpus through qpdf, Poppler, and Ghostscript tiffsep.

Roadmap

The public beta intentionally keeps plate aliasing, preview changes, explicit process conversion, and paint removal as separate operations. Current priorities are atomic multi-spot recipes, simpler package distribution, and carefully bounded support for more PDF constructs. New constructs are added only when they can retain the same fail-closed and post-save verification guarantees.

Project policy

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

spotpdf-0.7.0.tar.gz (401.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

spotpdf-0.7.0-py3-none-any.whl (124.1 kB view details)

Uploaded Python 3

File details

Details for the file spotpdf-0.7.0.tar.gz.

File metadata

  • Download URL: spotpdf-0.7.0.tar.gz
  • Upload date:
  • Size: 401.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for spotpdf-0.7.0.tar.gz
Algorithm Hash digest
SHA256 acc6e37f08354167b83294d08d3bf55ec779c613a2b90d91a8c22c0445b5b229
MD5 5b86b96c1de0f99dac9e670fcec9d712
BLAKE2b-256 e4f405c3b2e53f66d77ccf16179e4be6c03f9cd6ac7b05f874fa648836c5bd9c

See more details on using hashes here.

Provenance

The following attestation bundles were made for spotpdf-0.7.0.tar.gz:

Publisher: ci.yml on Hyperrick/spotpdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file spotpdf-0.7.0-py3-none-any.whl.

File metadata

  • Download URL: spotpdf-0.7.0-py3-none-any.whl
  • Upload date:
  • Size: 124.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for spotpdf-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4a5a1a4babe9f634fb9054081a63eebc78671cef394d8f48ce35048339d60ce3
MD5 acffc2acc6b97fcf85fc4ae68b5da0c5
BLAKE2b-256 75078210a58abe3c1f23fe60c4c729661a2cc2c526ff65d58b5cc6f711f6ab9e

See more details on using hashes here.

Provenance

The following attestation bundles were made for spotpdf-0.7.0-py3-none-any.whl:

Publisher: ci.yml on Hyperrick/spotpdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.8.0

2 files

0.7.1

2 files

This release

0.7.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page