*Epic CLI Manual — [index](../MANUAL.md)*

# 19. API service contract (file in, files out)

*Status: DRAFT, 2026-10-20. Written for the PHP service that will expose the jar as a
metered prepress API. Sections marked **(new, 1.40.0)** … **(new, 1.44.0)** describe jar
behaviour added for the service; all six items in §19.8 are built and, as of the
2026-10-20 roll, on every fleet box as 1.44.0+8200fef. Where this
chapter and the jar disagree, the jar wins — run `--help=<topic>` on the build you have.*

## 19.1 Principles

1. **The service only moves files.** Every endpoint takes a file (plus parameters), runs one
   jar invocation, and returns files plus one JSON result. Nothing in the service prints,
   spools, or touches a printer. `--spool` is local-only and is **not exposed**.
2. **One request = one jar process = one working directory.** The service creates a fresh
   directory per request, puts the input and any attached fonts in it, runs the jar with that
   directory as CWD, collects the outputs, and deletes the directory. Nothing persists.
3. **API requests never use machine fonts.** A font is either embedded in the PDF (subset is
   enough) or attached to the request. Anything else is an error that names the fonts. Jesse's
   own EpicERP runs keep the machine-font chain of [§3](03-configuration-aws-mysql-fonts.md);
   the strictness is a per-call switch the service always passes, never a jar default.
4. **Billing is counted, never measured.** The jar reports deterministic work counts (trim
   area, plates/pages/sheets, input size). The service turns them into units with a published
   table. CPU time and memory are reported alongside for calibration only and are never on a
   bill. The same file with the same parameters produces the same count on any machine.
5. **Additive only.** Every JSON shape here is a contract: fields are added, never renamed or
   removed. Parse `status`, never the count of warnings.

## 19.2 Running the jar

```
cd <request-dir> && java -Xmx<heap> -jar java-epic-cli.jar \
    --usage=usage.json --strict-fonts --fonts-dir=fonts <mode and options>
```

| Item | Rule |
|---|---|
| JVM | Temurin/OpenJDK 8, 11, 17 or 23. **Never the JDK 21 line** (`--extract-objects` fragments 55×). |
| Heap | `-Xmx768m` covers most PDF modes; `-Xmx2g` for `--separate`/`--vectorize` on large rasters. Scale with input megapixels. |
| CWD | The request directory. It must contain **no** `fonts/` directory other than the attached-fonts one named by `--fonts-dir`, and no `aws.ini`/`mysql.ini`. |
| Timeout | The service owns the wall-clock limit and kills the process on expiry. A killed process writes no `usage.json`; bill the base fee and report a timeout. |
| Streams | stdout = exactly one JSON document (the mode result). stderr = diagnostics. **Never merge them.** Capture both. |
| Exit code | See §19.7. Always read it; a partial output file with a non-zero exit is a failure. |
| Temp | The jar uses the system temp directory for bounded scratch; give it a writable one. |

### Request directory layout

```
<request-dir>/
  input.<ext>          the customer file (pdf, png, tif, jpg ...)
  fonts/               attached fonts, any filenames, .ttf/.otf (optional)
  usage.json           written by the jar (--usage)
  out/                 outputs the service asked for (--output=out/ or out/<name>)
```

The service names inputs and outputs; the jar never chooses a path the service did not give it
except where a mode documents its own naming (`--separate` plate files, `--split` page files,
`--thin-line` overlays), and those are always under the directory the service passed.

## 19.3 Global flags the service passes on every call

| Flag | Status | Meaning |
|---|---|---|
| `--usage=FILE` | **new, 1.40.0** | Write the usage sidecar (§19.5) to FILE on every exit path, success or failure. Absent = no sidecar, nothing else changes. Never written to stdout, because three modes (`--spool`, `--compile`, `--vectorize`) have stdout contracts consumers parse. |
| `--strict-fonts` | **new, 1.40.0** | A font that is not embedded and not found in `--fonts-dir` is an **error**, in every mode that reads glyphs or renders text (preflight is exempt: it reports, it does not render). The `./fonts` and `/datahandler/fonts` fallbacks of §3 are **off** under this flag; only the explicit `--fonts-dir` is searched. PDFBox's own substitute faces (Helvetica/Times/Courier look-alikes) are never used. Implies `--outline-missing=error` for `--outline-text`. |
| `--fonts-dir=DIR` | exists | The attached-fonts directory. Matched by the font's PostScript name (and family+style for `--compile` refs), not by filename. Omit when the request attached no fonts. |


`--quiet` is **never** passed: the service needs the result JSON.

`--unit=in|pt|mm|cm` is **not** an every-call flag. It is accepted only by the prepress transform
modes that take bare lengths (the modes listed above it in `--help`; default `in`, stroke weights
always `pt`). Every other mode, `--preflight` included, rejects it with exit 1
`Unknown option: --unit=...`. Pass it only with those modes, to pin the unit of bare numbers.

Shared modes such as `--preflight` take the **PDF itself**, not a master package ZIP: a ZIP is
exit 2 "damaged or not a PDF" (§15). Unpack packages at intake.

### What `--strict-fonts` reports **(new, 1.40.0)**

On a missing font the mode's result is `{"status":"error","reason":"missing fonts: <PSName>[, <PSName>...]", ...}` with exit **1**, and the usage sidecar carries the same list in `error.missing_fonts[]`. Fonts that were attached but never matched are listed in `fonts.attached_unused[]` so the customer can see they uploaded the wrong face. A licence-restricted face (OS/2 `fsType` forbids embedding) attached to the request is reported in `error.missing_fonts[]` too, with `fonts.restricted[]` naming it: it is not used, ever.

**`--preflight` reports, it never refuses (fixed 2.14.1).** Under `--strict-fonts` the preflight
report is the same JSON the call gives without the flag. A non-embedded font appears in
`pages[].fonts` as `{"name":...,"embedded":false}`, `liveTextExists` is true, and the exit code
is 0. The usage sidecar's `fonts.missing` names each face that is neither embedded nor attached,
so intake builds its missing-font list from either place. An attached face that matches is used,
and listed in `fonts.attached_used`. **Through 2.14.0 this was broken:** the refusal was thrown
inside the best-effort page interrogation, so a page using a missing font silently lost
`liveTextExists`, `fonts`, `vectorElementsExist`, `strictlyVector`, raster images and all
thin-line data. Exit was still 0 and stdout carried no error.

## 19.4 Operation catalogue

Each endpoint is one row. "Result" is the stdout JSON; the manual chapter holds the full field
list. CMYK in colour terms is 0–100 percent; list separators are `;`.

### Inspect (file in, JSON out, nothing written)

| Endpoint | Jar command | Result / chapter | Billing counts |
|---|---|---|---|
| `preflight` | `--preflight input.pdf` | §4.1 report: boxes, fonts (embedded/subset), spot and process colours, raster inventory, `minimumEffectiveDpi`, variables. | pages |
| `preflight` + thin lines | `--preflight --thin-line=P:N input.pdf out/tl_%d.png` | §4.1 + §4.4 `thinLine`/`thinLineContour` per page, one overlay PNG per page. | pages |
| `colors` | `--list-spot-colors input.pdf` / `--list-process-colors input.pdf` | §4.2 / §4.3 | pages |
| `ink-coverage` | `--ink-coverage input.pdf [--dpi=N] [--plates=A;B] [--tac-check[=N]]` (`--tac-check` **new, 1.43.0**) | §17.6: `pages[].plates[{name,kind,coverage_pct,blank}]`, every `file` null; with `--tac-check` also `pages[].tac{..}`, `tac_max_pct`, `tac_over`. | pages × plates |
| `quantize` | `--quantize=N input.png [--paper=TERM]` | §18.2: the N dominant colours three ways plus `colors_spec`. | megapixels |
| `match-colors` | `--match-colors input.pdf\|png --palette=book.ase [--quantize=N] [--max-de=N]` (**new, 1.42.0**) | §18.2: nearest book entry per spot / dominant colour with dE; the book is an attached file. | pages / megapixels |
| `gang-check` | `--gang-check a.pdf b.pdf [--rotation=..]` | §6: exit 4 = cannot share a rotation. | pages |
| `separate --dry-run` | `--separate ... --dry-run` | §18.9: the whole plan (inks, passes, screens, files it would write) with nothing written. **This is the quote call for separations.** | pages |

### Edit (PDF in, PDF out)

| Endpoint | Jar command | Chapter |
|---|---|---|
| `resize` | `--resize=WxH input.pdf out/x.pdf` (`8.5x` / `x11` proportional) | §5.1 |
| `mirror` / `flip` / `rotate` | `--mirror`, `--flip`, `--rotate90\|180\|270` (chainable, in order) | §5.1 |
| `artboard` | `--artboard=WxH [--anchor=A]` / `--artboard=ink[+PAD]` | §5.1a |
| `bleed` | `--bleed=N [--keep-trim]` | §5.1a |
| `crop` | `--crop [--artboard=..] [--bleed=..]` | §5.1a |
| `fit` / `scale` / `align` / `offset` / `rotate-art` | §5.1b art-moving transforms | §5.1b |
| `distort` | `--distort=shrink:X%,Y%` (art transform) / `--distort=cone:TOP,BOTTOM,HEIGHT[unit] [--distort-ref=..] [--dpi=N]` on a PDF or picture (**new, 1.44.0**) | §5.1b / §17.6b |
| `pages` / `delete-pages` / `merge` | `--pages=SPEC`, `--delete-pages=SPEC`, `--merge a.pdf b.pdf --output=out/x.pdf` | §17.2 |
| `step` | `--step=RxC --gap=N --sheet=WxH` | §17.2 |
| `tile` | `--tile=WxH [--tile-overlap=N] [--tile-hem=N] [--tile-grommets=N]` (**new, 2.27.0**) | §17.2a |
| `signature` | `--signature=4\|8\|16\|32 [--sig-binding=perfect\|saddle] [--sig-press=sheetwise\|perfecting\|turn\|tumble] [--sig-creep=N]` (**new, 2.28.0**) | §17.2b |
| `compare` | `--compare a.pdf b.pdf [--compare-overlay=FILE.png] [--compare-dpi=N] [--compare-tolerance=PX]` (**new, 2.29.0**) | §17.2c |
| `marks` | `--marks[=crop,reg,center,bleed] [--bleed=N] [--marks-color=TERM]` | §17.3 |
| `keyline` | `--keyline[=TERM] [--keyline-box=..] [--keyline-dash=A,B]` | §17.3 |
| `edge` | `--edge=TERM --edge-width=N` | §17.4 |
| `expand-strokes` | `--expand-strokes [--hairline=PT]` | §17.5 |
| `outline-text` | `--outline-text` (strict: a non-embedded, non-attached font is an error) | §17.5 |
| `spot-to-process` / `convert-cmyk` | `--spot-to-process[=Name;Name]`, `--convert=cmyk [--tac=N] [--profile=[SRC.icc,]DST.icc [--intent=perceptual\|relcol\|sat\|abs]]` (profile from 2.22.0) | §17.6 |
| `overprint` | `--overprint=on\|off [--opm=0\|1]` | §17.6 |
| `output-intent` | `--output-intent=FILE.icc[,id=..][,info=..][,subtype=pdfx\|pdfa\|pdfe]` or `--output-intent=remove` (2.23.0) | §17.6 |
| `pdfx-check` | `--pdfx-check=x1a\|x4 [--pdfx-fix [--trapped=true\|false] [--output-intent=FILE.icc]]` (2.24.0); a house preflight against the PDF/X rule list, not an ISO 15930 certificate | §17.6 |
| `wrap` | `--wrap <image> --output=out.pdf [--dpi=N\|XxY]` (**new, 2.32.0**); a PNG/JPEG/TIFF as a one-page PDF at its own DPI; `pages_out` 1 | §17.6c |
| `links-check` | `--links-check FILE.pdf\|PACKAGE.zip [--links=DIR]` (**new, 2.35.0**; zip input 2.37.0); an Illustrator master's placed-file records against its Links/ folder; exit 1 when any is missing; writes nothing | §17.6d |
| `relink` | `--relink FILE.pdf\|PACKAGE.zip [--links=DIR] [--output=OUT.pdf\|OUT.zip] [--package-root=DIR]` (**new, 2.35.0**; zip input 2.37.0); rewrites every placed-file record to the package's Links/ (absolute + relative path) as Illustrator's Package does; page content untouched | §17.6d |
| `retype` | `--retype=SPEC.json FILE.pdf --output=OUT.pdf` / `--report-only` (**new, 2.36.0**); removes the traced subpaths inside a region and sets live text there (font embedded whole, Illustrator data re-authored); report-only returns the outlines as objects[] | §17.6e |
| `remove-color` | `--remove-color=NAME[;NAME] [--match=exact\|ci\|glob]` | §17.7 |
| `swap-spot-colors` | `--swap-spot-colors="Old=New:C,M,Y,K"` (0–100 percent like every other colour flag; an earlier revision of this row said 0–1, which was wrong — measured §5.3) | §5.3 |
| `split` | `--split input.pdf out/page_%d.pdf` | §5.2 |
| `impose` | `--imposition=plan.json` (plan written by the service; `gang_check` optional) | §6 |
| `trap` | `--separate --trap=N[:MODE] input.pdf\|png --output=out/` (**new, 1.41.0**) | §18.3 |

### Separate and screen (PDF or raster in, plates out)

| Endpoint | Jar command | Chapter |
|---|---|---|
| `separate` (spot, from PDF) | `--separate input.pdf --output=out/ [--dpi=N] [--plates=..]` | §18.1 |
| `separate-cmyk` | `--separate input.png --colors=process[:GCR] --profile=naive\|icc [--tac=N]` | §18.3 |
| `separate-spot` (N inks / palette / fixed) | `--separate input.png --colors=N \| "TERM;TERM" [--palette=..]` | §18.3 |
| `separate-sim-process` | `--separate input.png --sim-process[=N] [--palette=book] [--paper=TERM]` | §18.3 |
| `separate-index` / `duotone` | `--index`, `--duotone=..` | §18.3 |
| underbase / choke / spread | `--underbase[=TERM][:MODE] [--choke=N] [--spread=N]` | §18.3 |
| halftone / film | `--halftone[=LPI:ANGLE:DOT] [--film] [--film-mirror=on] [--invert=A;B] [--gang-films=WxH]` — the 1-bit film files **are** the output | §18.4, §18.7 |
| proof | `--proof [--proof-view=garment,sheet] [--paper=TERM]` | §18.5 |
| screen check | `--screen-check [--mesh=N]` | §18.6 |
| plates PDF | `--pdf` | §18.10 |
| shop profile | `--shop=FILE` written by the service from the customer's stored settings | §18.8 |

### Generate

| Endpoint | Jar command | Chapter |
|---|---|---|
| `vectorize` | `--vectorize --input=art.png --output=out/art.pdf [--report=out/art.json] [options]` | §8 |
| `extract-objects` | `--extract-objects input.pdf --report=out/r.json [--with-text]` | §9 |
| `simplify-curves` | `--simplify-curves[=PT] input.pdf out/x.pdf` | §10 |
| `backers` | `--backers=input.pdf --plates="..." --output=out/x.pdf` | §11 |
| `compile` | `--compile=out/request.json` (request written by the service; fonts via `design.fonts[]` resolved in `--fonts-dir`) | §7 |
| `derivatives` / `derivative-preview` | `--derivatives=pkg.zip --output=out/`, `--derivative-preview=pkg.zip` | §12 |
| `render` | `--render[=DPI] input.pdf out/x.png [--box=..] [--render-matte=..]` | §17.2 |
| `soft-proof` | `--render[=DPI] input.pdf out/x.png --soft-proof=PRESS.icc\|intent [--soft-proof-intent=..] [--soft-proof-paper=on]` (also on `derivative-preview`) | §17.6a |

### Not exposed

`--spool` (local printer), `--aws`/`s3://` paths (the service moves files itself), `--mysql`,
`--quiet`, `--help`, `--version` (the service reads the version once at startup and records it
in every usage sidecar).

## 19.5 The usage sidecar **(new, 1.40.0)**

Written to the `--usage=FILE` path on **every** exit, including usage errors, faults and
`--strict-fonts` refusals. The only case with no sidecar is a process killed from outside.

```json
{
  "schema": 1,
  "jar": "1.40.0+abc1234",
  "mode": "separate",
  "argv": ["--separate", "input.pdf", "--output=out/", "--dpi=600", "--usage=usage.json"],
  "status": "ok",
  "exit_code": 0,
  "error": null,
  "work": {
    "input": "input.pdf",
    "pages_in": 1,
    "pages_out": 6,
    "files_out": 6,
    "trim_in": { "width": 14.0, "height": 16.0 },
    "trim_sq_in": 224,
    "plates": 6,
    "sheets": 0,
    "placed_sq_in": 0.0,
    "datasets": 0,
    "image_swaps": 0,
    "bytes_in": 1843200,
    "megabytes_in": 2,
    "bytes_out": 5210000,
    "raster_megapixels": null
  },
  "fonts": {
    "strict": true,
    "embedded": ["Bungee-Regular"],
    "not_embedded": ["Oswald-Bold"],
    "attached": ["Oswald-Bold"],
    "attached_used": ["Oswald-Bold"],
    "attached_unused": [],
    "restricted": [],
    "missing": []
  },
  "resources": { "cpu_ms": 4120, "wall_ms": 4390, "monotonic_ms": 4390, "peak_heap_mb": 612 },
  "warnings": 1,
  "stdout_truncated": false
}
```

| Field | Meaning | On the bill? |
|---|---|---|
| `mode` | The first `--flag` on the command line that is not a connection/output control (`--usage`, `--usage-input`, `--threads`, `--strict-fonts`, `--fonts-dir`, `--quiet`, `--unit`, `--aws`, `--mysql`). Through 2.3.0 only the last four were skipped, so a command that began `--usage=u.json` recorded mode `usage`. | — |
| `argv` | The command line as received, before the global flags were stripped. | — |
| `status`, `exit_code` | From the mode's result JSON and the exit funnel. A mode that prints a JSON array (`--list-spot-colors`) infers `status` from the exit code. | — |
| `error` | `null`, or `{ "reason": "...", "missing_fonts": [...] }` on failure. | — |
| `work.input` | The file billed: `--usage-input`, else `--input=`/`--file=`, else the first positional that exists, else the first `--flag=FILE` that exists and is not a side file (`--shop`, `--palette`, `--request`…). | — |
| `work.pages_in` | Pages in the input (1 for a raster). | yes |
| `work.pages_out` | Pages in every PDF written plus one per non-PDF file written. | yes |
| `work.files_out` | Files written: under `--output`/`--report` (file, directory or `%d` pattern), the second positional, sidecars beside a named output, an `--imposition` plan's `destination` (since 2.3.1), or the input itself for an in-place edit. A file counts when its mtime is at or after the run started; the input counts only when this run changed its mtime (since 2.3.1: a request or plan written just before the call is not an output, and through 2.3.0 an input given by a RELATIVE path was counted twice, doubling `files_out`, `pages_out` and `bytes_out` for in-place edits). | shown |
| `work.trim_in` | The trim box of page 1 in inches, page rotation applied (PDFBox falls back crop → media; a raster uses its pixel size at `--dpi`, default 300). Exact, not rounded. | shown |
| `work.trim_sq_in` | `ceil(width) × ceil(height)` of the trim box, whole inches. `--imposition` (its input is a plan, not a page): the sum of that product over every sheet written, and `trim_in` is the first sheet's size. Since 2.3.1. | yes |
| `work.plates` | Top-level `plates[]` of the result (`--backers`, plates PDF), else the sum of `pages[].plates[]` (`--separate`, `--ink-coverage`); otherwise 0. | yes |
| `work.sheets`, `work.datasets` | `sheets[]` / `datasets[]` of the result (`--gang-films`, `--derivatives`), plus, for `--imposition`, the sheets written (one per plan; one per flow-layout block, a `preview` writes none; since 2.3.1); otherwise 0. | yes |
| `work.placed_sq_in` | `--imposition` only: square inches of art placed, the sum of `pdf_documents[].bounding_box` width × height (a flow-layout sheet is cropped to its ink, so all of its area counts). Exact, not rounded up. Since 2.3.1; otherwise 0. | shown |
| `work.image_swaps` | Image-variable placements (chapter 12) in datasets that produced output: `--derivatives`, `--derivative-preview`, and per request in `--serve`. A dataset that fails on its second image does not count its first. Since 2.2.0; otherwise 0. | yes |
| `work.items`, `work.candidates`, `work.runs` | `--pack` only (chapter 21), and only on a run that produced a result: items in the request, candidate run options evaluated (the result's `evaluated`), and runs in the recommended strategy. Absent for every other mode. Since 2.4.0. A roll request (2.26.0) reports candidates = cylinder/die options scored and runs = roll runs; its `--die-pdf` counts in `files_out` / `pages_out` / `bytes_out`. A nest request (2.30.0) reports items = request items, candidates = anchor rows tested (the result's `evaluated`) and runs = sheets used; its `--nest-pdf` counts the same way. | — |
| `work.bytes_in`, `work.megabytes_in` | Input file size; megabytes is `ceil(bytes / 1048576)`. | yes (MB) |
| `work.bytes_out` | Total size of every file written. | shown |
| `work.raster_megapixels` | Input pixels in millions for a raster input; `null` for a PDF. | calibration |
| `fonts.embedded`, `fonts.not_embedded` | BaseFont names found in the input PDF's resources (form XObjects included), by whether a font program is embedded. Read without loading the fonts. | shown |
| `fonts.strict`, `attached`, `attached_used`, `attached_unused`, `restricted`, `missing` | The `--strict-fonts` ledger; the `attached*` lists are `null` without the flag. `missing` is what the refusal named. | shown |
| `resources.*` | Process CPU, wall, monotonic and peak heap. **Calibration only, never billed.** | no |
| `stdout_truncated` | `true` when the result JSON exceeded the 16 MB capture and the counts taken from it are 0. | — |

Rules: every number is computed from the input and the files written, never from clocks
except inside `resources`. Clock reads happen once at install and once at exit, never inside
loops. Counters are per process, and the service runs one process per request, so they can
never mix customers. The sidecar is written by a JVM shutdown hook, so an uncaught error still
produces one (`exit_code` 2, `error.reason` from the result if any).

## 19.6 Billing formula

Published on the website, one line, every term visible to the customer before upload:

```
units = base + area_rate × trim_sq_in × count + size_rate × megabytes_in
```

- `trim_sq_in` is the whole-inch trim area from the sidecar.
- `count` is `max(1, plates + sheets + pages_out)` for the operation: plates for separations,
  sheets for imposition and step, pages for everything else.
- `megabytes_in` is the input size rounded up.
- `base`, `area_rate`, `size_rate` are per operation and live only in the service's price
  table. Changing a price never needs a jar roll.

**The bill is the explanation.** Every usage report the service returns prints the arithmetic
with the numbers substituted (`50 + 2 × 224 × 6 + 1 × 2 = 2740 units`). No complexity tiers, no
DPI multiplier, no measured time anywhere on a bill.

**Quote before work.** The service computes the quote from a `preflight` call (pages, trim
box, spot inks, file size) plus the requested operation. For separations the exact plate count
comes from `--separate ... --dry-run`. A quote is binding: the run bills what the quote said
unless the customer changed the parameters.

**Failures.** Customer-fault failures (missing fonts, unreadable file, usage error) bill the
base fee only. Service faults (exit 2 not caused by the input, timeouts, crashes) bill nothing.
Both rules are published.

## 19.7 Exit codes and the service's error mapping

| Exit | Jar meaning | Service treatment |
|---|---|---|
| 0 | Success (for `--derivatives`, includes partially failed batches: read per-dataset status). | 200, files + result + usage |
| 1 | Usage error, failed `--compile`/`--simplify-curves`, **`--strict-fonts` refusal**. The result JSON on stdout says why. | 422, `reason` from the result, base fee |
| 2 | File/environment fault (unreadable input, package). | 422 if the input is at fault, else 500; base fee / nothing |
| 3 | `--imposition` recolor named a separation the file does not have (message lists the real ones). | 422, base fee |
| 4 | Print-order conflict (`--gang-check`, plan `gang_check: refuse`). | 409 with the conflict JSON, base fee |
| killed | Service timeout. | 504, nothing billed |

Every standalone mode's error path still emits a well-formed JSON object on stdout; key on
`status`. Under `--strict-fonts` the refusal is always exit 1 with `reason` starting
`missing fonts:`. The rvp modes (`--extract-objects`, `--vectorize`) have no JSON error path;
there the refusal is the one stderr line `epic-rvp: missing fonts: NAME` (2.37.1), and
`--extract-objects` without `--with-text` reports `not liftable: live text` instead, since
outlining is the fix either way.

## 19.8 What is being built for the service (1.40.0 and after)

Checked against the jar on 2026-10-20. Items that an earlier draft of this plan called
"missing" but that already exist: text to outlines (`--outline-text`), ink coverage
(`--ink-coverage`), TAC cap (`--tac`), 1-bit film with mirror/negative (`--halftone`,
`--film`), soft proof on garment colour (`--paper`, `--proof-view`), rasterize (`--render`),
underbase choke/spread, shop profiles, dry run.

| # | Item | Flag | Notes |
|---|---|---|---|
| 1 | Usage sidecar | `--usage=FILE` | §19.5. Global, stripped before mode parsing like `--fonts-dir`. **Built, 1.40.0.** |
| 2 | Strict fonts | `--strict-fonts` | §19.3. A font mapper that refuses fallbacks; attached dir searched by PostScript name or filename stem; `fonts.*` in the sidecar. **Built, 1.40.0.** |
| 3 | Trapping on plates | `--separate --trap=N[:MODE]` | Spread the lighter ink under the darker by N source pixels wherever two plates abut, on PDF and raster plates alike; `MODE` = `spread` (default) \| `choke` \| `centre`. Darker-knocks-lighter by ink luminance, paper and underbase excluded, `--trap-pairs="A>B;.."` to override. Reports `trap{px, mode, pairs[{page, under, over, pixels}]}`. Vector trapping of the PDF itself is out of scope. §18.3. **Built, 1.41.0.** |
| 4 | Nearest ink from a book | `--match-colors --palette=BOOK input.pdf\|png` | For every spot colour (PDF) or dominant colour (`--quantize` set) the nearest entry of the customer's own book (ASE / `--colors` grammar / library), with dE, `--max-de` → `unmatched[]`, `colors_spec` for `--separate`. The jar ships no Pantone data; the book is the customer's. §18.2. **Built, 1.42.0.** |
| 5 | Total ink coverage measure | `--ink-coverage --tac-check[=N]` | Adds per page `tac{limit_pct, max_pct, mean_pct, over_pct, over_pixels, inked_pixels, pixels, plates[]}` and top-level `tac_max_pct`, `tac_over`, additive to the §17.6 result; also on `--separate` (after `--trap`). §17.6. **Built, 1.43.0.** |
| 6 | Pre-distortion | `--distort=cone:TOP,BOTTOM,HEIGHT[unit]` / `--distort=shrink:X%,Y%` | Shrink is an art transform (1/(1−X) × 1/(1−Y) about the anchor, artboard fixed, `shrink_pct` in the result, §5.1b). Cone bends every path of the page (text outlined and strokes expanded first) into the annular sector of the vessel, page boxes follow; images/shadings/shared forms refuse with exit 1; pictures are inverse-mapped to RGBA PNG. §17.6b. **Built, 1.44.0.** |

Out of scope for the service: garment placement templates (an imposition plan does it), SVG/EPS
export, transparency flattening as a standalone action (`--separate` resolves transparency at
render time, which is what a plate needs).

## 19.9 Version gate

The service records `--version` at startup and refuses to start on a jar older than the one
this chapter was written against once `--usage` ships: an older jar would silently produce no
sidecar and nothing to bill. Every usage sidecar carries `jar` so a bill can always be traced
to the build that produced it.
