Metadata-Version: 2.4
Name: spotpdf
Version: 0.7.0
Summary: Inspect and safely mutate named spot colors in vector PDF content.
Author: spotpdf contributors
License-Expression: MIT
Project-URL: Documentation, https://github.com/Hyperrick/spotpdf#readme
Project-URL: Changelog, https://github.com/Hyperrick/spotpdf/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/Hyperrick/spotpdf/issues
Project-URL: Repository, https://github.com/Hyperrick/spotpdf
Project-URL: Security, https://github.com/Hyperrick/spotpdf/security/policy
Project-URL: Support, https://github.com/Hyperrick/spotpdf/blob/main/SUPPORT.md
Keywords: pdf,spot-color,separation,prepress,printing
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Manufacturing
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Printing
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pikepdf<11,>=10.12
Dynamic: license-file

# spotpdf

[![CI](https://github.com/Hyperrick/spotpdf/actions/workflows/ci.yml/badge.svg)](https://github.com/Hyperrick/spotpdf/actions/workflows/ci.yml)
![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-3776AB)
[![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/Hyperrick/spotpdf/blob/v0.7.0/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`](https://github.com/Hyperrick/spotpdf/blob/v0.7.0/examples/create_demo_pdf.py). No customer or
production files are included in this repository.

<table>
  <thead>
    <tr>
      <th>Before: process artwork plus three spots</th>
      <th>After: <code>remove --all</code></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://github.com/Hyperrick/spotpdf/blob/v0.7.0/docs/images/demo-before.png"><img src="https://raw.githubusercontent.com/Hyperrick/spotpdf/v0.7.0/docs/images/demo-before.png" alt="Synthetic vector PDF before processing, with a blue process-color block, pink varnish fill, pink cut contour, and green personalization text"></a></td>
      <td><a href="https://github.com/Hyperrick/spotpdf/blob/v0.7.0/docs/images/demo-after.png"><img src="https://raw.githubusercontent.com/Hyperrick/spotpdf/v0.7.0/docs/images/demo-after.png" alt="Synthetic vector PDF after spot removal; the blue process-color block remains while varnish, cut contour, and personalization text are gone"></a></td>
    </tr>
  </tbody>
</table>

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

<a href="https://github.com/Hyperrick/spotpdf/blob/v0.7.0/docs/images/demo-convert.svg"><img src="https://raw.githubusercontent.com/Hyperrick/spotpdf/v0.7.0/docs/images/demo-convert.svg" alt="Terminal walkthrough showing the Varnish spot plate converted to explicit DeviceCMYK while unrelated spots remain and the equivalent composite render stays pixel-identical"></a>

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

<a href="https://github.com/Hyperrick/spotpdf/blob/v0.7.0/docs/images/demo-rename.svg"><img src="https://raw.githubusercontent.com/Hyperrick/spotpdf/v0.7.0/docs/images/demo-rename.svg" alt="Terminal walkthrough showing Varnish renamed to Varnish Renamed while all other spot plates stay unchanged and the render remains pixel-identical"></a>

### 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`.

<table>
  <thead>
    <tr>
      <th>Original alternate preview</th>
      <th>After: <code>set-alternate</code></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://github.com/Hyperrick/spotpdf/blob/v0.7.0/docs/images/demo-before.png"><img src="https://raw.githubusercontent.com/Hyperrick/spotpdf/v0.7.0/docs/images/demo-before.png" alt="Synthetic PDF before the Varnish alternate preview is changed"></a></td>
      <td><a href="https://github.com/Hyperrick/spotpdf/blob/v0.7.0/docs/images/demo-alternate.png"><img src="https://raw.githubusercontent.com/Hyperrick/spotpdf/v0.7.0/docs/images/demo-alternate.png" alt="The same synthetic PDF after only the Varnish alternate preview is changed from magenta to cyan"></a></td>
    </tr>
  </tbody>
</table>

## Install

Python 3.11 or newer is required. Install the latest stable release as an
isolated command with [`uv`](https://docs.astral.sh/uv/):

```bash
uv tool install spotpdf==0.7.0
spotpdf --version
```

Or use `pipx`:

```bash
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:

```bash
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:

```bash
spotpdf list input.pdf
```

Default text output from the synthetic demo:

```text
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:

```bash
spotpdf --format json list input.pdf
```

```json
{"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](https://github.com/Hyperrick/spotpdf/blob/v0.7.0/docs/json-output.md) 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:

```bash
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](https://github.com/Hyperrick/spotpdf/blob/v0.7.0/docs/performance.md).

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

```bash
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](https://github.com/Hyperrick/spotpdf/blob/v0.7.0/docs/compatibility.md) for the deliberately unsupported
prepress structures and the relevant sections of the
[official Adobe-hosted ISO 32000-1:2008 copy](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf).
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:

```bash
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:

```bash
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](https://github.com/Hyperrick/spotpdf/blob/v0.7.0/docs/compatibility.md) for the exact initial subset.

The same operation is available as a library API:

```python
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](https://github.com/Hyperrick/spotpdf/blob/v0.7.0/docs/python-api.md)
for the canonical root imports, typed result records, controlled error
hierarchy, and compatibility policy.

Remove supported paint for one name:

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

Remove all supported named spots in one atomic rewrite:

```bash
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:

```bash
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:

```bash
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:

```bash
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](https://github.com/Hyperrick/spotpdf/blob/v0.7.0/docs/processing-budgets.md) and
[security policy](https://github.com/Hyperrick/spotpdf/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](https://github.com/Hyperrick/spotpdf/blob/v0.7.0/docs/troubleshooting.md).

## 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](https://github.com/Hyperrick/spotpdf/blob/v0.7.0/docs/compatibility.md) 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:

```bash
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:

```bash
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`:

```bash
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](https://github.com/Hyperrick/spotpdf/blob/v0.7.0/docs/architecture.md) for module boundaries and
the mutation pipeline. Publication checks and processing budgets reduce risk;
they do not make the process a sandbox.

## Development

```bash
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](https://github.com/Hyperrick/spotpdf/blob/main/CONTRIBUTING.md). Releases additionally run the pinned
[public prepress corpus](https://github.com/Hyperrick/spotpdf/blob/v0.7.0/docs/public-corpus.md) 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

- [Changelog](https://github.com/Hyperrick/spotpdf/blob/v0.7.0/CHANGELOG.md)
- [Contributing](https://github.com/Hyperrick/spotpdf/blob/main/CONTRIBUTING.md)
- [JSON output and automation](https://github.com/Hyperrick/spotpdf/blob/v0.7.0/docs/json-output.md)
- [Processing budgets](https://github.com/Hyperrick/spotpdf/blob/v0.7.0/docs/processing-budgets.md)
- [Python API](https://github.com/Hyperrick/spotpdf/blob/v0.7.0/docs/python-api.md)
- [Public corpus and RIP checks](https://github.com/Hyperrick/spotpdf/blob/v0.7.0/docs/public-corpus.md)
- [Release process and artifact verification](https://github.com/Hyperrick/spotpdf/blob/main/docs/releasing.md)
- [Security policy](https://github.com/Hyperrick/spotpdf/security/policy)
- [Support and issue routing](https://github.com/Hyperrick/spotpdf/blob/main/SUPPORT.md)
- [MIT License](https://github.com/Hyperrick/spotpdf/blob/v0.7.0/LICENSE)
