spotpdf
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.
Who it is for — and why it exists
spotpdf is for prepress engineers, print-production teams, lettershops, and
developers building automated PDF workflows. It helps when a job requires a
repeatable way to inventory named plates, remove supported spot-painted text or
vector objects, rename a plate, change its composite preview, or convert a known
/Separation recipe to explicit CMYK without rasterizing the page.
Open-source PDF libraries can detect named colorants, but safe object-level
editing of their actual paint is poorly covered. spotpdf exists to turn that
inspection into deterministic CLI and Python operations with dry runs, JSON
reports, and validated outputs—without requiring a proprietary desktop
preflight product.
Locate failures visually
Request a self-contained HTML report with original-page previews, numbered object locations, enlarged excerpts, and structured error details:
spotpdf remove input.pdf --spot Varnish --dry-run --report report.html
The report also works with rename, set-alternate, and convert. It distinguishes
the actual failure from additional findings and clearly labels locations that can
only be traced to a page or resource. PDFium and Pillow are included with installation;
rendering runs only when a report is requested. See the
diagnostic report guide for limits and examples.
Try a realistic print example
This fictional, two-page English coffee brochure contains real text, vector
artwork, and two FOIL_GOLD spot-color seal images. Removing these images is
intentionally unsupported: the report locates both seals on their original pages.
The finding links the failure to its PDF object and shows an enlarged excerpt:
Generate the PDF and report from a checkout:
uv sync --locked --dev
uv run python examples/create_report_demo.py out/nord-coffee.pdf
uv run spotpdf remove out/nord-coffee.pdf --spot FOIL_GOLD \
--dry-run --report out/nord-report.html
The last command intentionally exits with code 1 and creates the report.
Open out/nord-report.html in a browser; all preview images are embedded and work
offline. The original PDF stays unchanged. The report explicitly lists any
unexamined areas; a located object does not imply exhaustive validation.
These are actual browser screenshots, generated from the example rather than mockups. See the example instructions for reproduction and screenshot capture. Only the generator and screenshots are versioned; no customer PDF is included.
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 |
|---|---|
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.
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.
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 |
|---|---|
Install
Python 3.11 or newer is required. Install the package in an active virtual
environment with pip:
python -m pip install spotpdf==0.8.0
For an isolated command-line installation, use
uv:
uv tool install spotpdf==0.8.0
spotpdf --version
Or use pipx:
pipx install spotpdf==0.8.0
Stable v0.8.0 contains list, check, remove, rename, set-alternate,
and the fail-closed convert command, plus verified mutation dry runs with
optional self-contained HTML diagnostic reports 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.8.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.8.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
- Check input bytes, open without parser recovery, and reject open-time warnings.
- Enforce page, graph, decoded-content, and operator budgets; then run qpdf's full syntax check and reject final warnings.
- Inventory reachable color declarations and exact-name dependencies.
- Build a complete rename, alternate-preview, or conversion plan, or perform a complete internal removal planning pass before changing the in-memory document.
- Apply only the planned slots or selected paint changes as one operation.
- Save beside the destination, reopen strictly, and verify the result.
- 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file spotpdf-0.8.0.tar.gz.
File metadata
- Download URL: spotpdf-0.8.0.tar.gz
- Upload date:
- Size: 640.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
35e5acc96a96550c110b60b766b7cfc8e46f304894117fd26b1e81284f290ca0
|
|
| MD5 |
b97fc4ddd8c2c390ffe7b9a0dc3ba3df
|
|
| BLAKE2b-256 |
c2826f1a77c9d6588459589b7b352eff8d095bf69d152a75e579831d3ea1e58e
|
Provenance
The following attestation bundles were made for spotpdf-0.8.0.tar.gz:
Publisher:
ci.yml on Hyperrick/spotpdf
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
spotpdf-0.8.0.tar.gz -
Subject digest:
35e5acc96a96550c110b60b766b7cfc8e46f304894117fd26b1e81284f290ca0 - Sigstore transparency entry: 2780591462
- Sigstore integration time:
-
Permalink:
Hyperrick/spotpdf@47765e11fda17a689bf031f0fe6ae2e93a871354 -
Branch / Tag:
refs/tags/v0.8.0 - Owner: https://github.com/Hyperrick
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@47765e11fda17a689bf031f0fe6ae2e93a871354 -
Trigger Event:
push
-
Statement type:
File details
Details for the file spotpdf-0.8.0-py3-none-any.whl.
File metadata
- Download URL: spotpdf-0.8.0-py3-none-any.whl
- Upload date:
- Size: 146.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
00153451dc51f46892093ddc9bbd79ab50dfc099d828b012aac805484843aa18
|
|
| MD5 |
a0433108f7f95d143458aaf75f4ce04b
|
|
| BLAKE2b-256 |
4ee97cd98d2f1cf7c71e4f0e20ce41dd89b652b48bcb2c2f446ff40ea525a8be
|
Provenance
The following attestation bundles were made for spotpdf-0.8.0-py3-none-any.whl:
Publisher:
ci.yml on Hyperrick/spotpdf
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
spotpdf-0.8.0-py3-none-any.whl -
Subject digest:
00153451dc51f46892093ddc9bbd79ab50dfc099d828b012aac805484843aa18 - Sigstore transparency entry: 2780591580
- Sigstore integration time:
-
Permalink:
Hyperrick/spotpdf@47765e11fda17a689bf031f0fe6ae2e93a871354 -
Branch / Tag:
refs/tags/v0.8.0 - Owner: https://github.com/Hyperrick
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@47765e11fda17a689bf031f0fe6ae2e93a871354 -
Trigger Event:
push
-
Statement type: