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

# 17. Prepress tools

*(1.14.0 – 1.23.0; `--help=prepress`, `--help=remove-color`)*

Jesse's 2026-10-12 directive: every singular prepress action is a dedicated flag, and because
the toolset serves many shops, every hard-coded shop value is a flag whose default is the old
value. Each tool here is a standalone mode: `<source> [output]`, **`[output]` omitted = overwrite
in place**, one JSON result on stdout, exit `0` ok / `1` usage / `2` fault. Every result carries
`status`, `source`, `output`, `warnings[]`; all fields are additive.

## 17.1 Units

Any length accepts a unit suffix: `in` (default), `pt`, `mm`, `cm`. `--unit=mm` (etc.) changes
what a **bare** number means for the whole command. Stroke weights (`--*-weight`, `--edge-width`,
`--hairline`, dash lengths) are **points** unless a unit is given. CMYK in colour terms is
**0–100 percent** everywhere in this chapter.

## 17.2 Pages, merge, render, step

| Flag | What it does | Result |
|---|---|---|
| `--pages=SPEC` | Keep the listed pages in this order (1-based). `1,3-5,2`, `last`, `7-` (to the end), `5-1` (reversed); a page may repeat. | `pages_before`, `pages`, `order[]` |
| `--delete-pages=SPEC` | Keep every page except SPEC. | same |
| `--merge a.pdf b.pdf [...] out.pdf` | Concatenate (or `--output=out.pdf`). Document-level Illustrator data follows the first file. `--strip-ai-payload` drops it so the file opens as plain PDF. | `sources[]`, `pages`, `order[]`, `ai_payload_stripped` |
| `--render[=DPI] in.pdf out.png` | RGBA PNG per page, **unmatted** (paper transparent, so white ink is visible), default 150 dpi, crop box. Pages after the first go to `out_2.png`… `--pages=1,3`, `--box=media\|crop\|trim\|art\|bleed`, `--render-matte=none\|white\|black\|#rrggbb`, `--render-format=png\|jpg` (jpg forces a matte), `--render-name=index\|page`, `--render-aa=on\|off`, `--render-layers=art\|all`. Since 2.9.0, `art` is the default: the `PRODUCTION_ONLY` layer is left out at any depth (on a page, or in art placed on an imposed sheet), which is what the customer sees. `all` paints it, which was the only behaviour before 2.9.0. Soft proof: `--soft-proof=FILE.icc\|intent` (§17.6a, 2.25.0). | `dpi`, `pages`, `files[{page,file,width_px,height_px}]`, `matte`, `layers`, `soft_proof` |
| `--step=RxC` | Step-and-repeat page 1's artboard R rows × C columns on one sheet: one form, R·C placements each clipped to its cell, colours/overprint/transparency untouched. `--gap=N\|H,V`, `--sheet=WxH`, `--margin=N`, `--anchor=A`, `--step-page=N`, `--step-box=`, `--step-clip=cell\|none`, `--step-rotate=none\|rows\|cols\|checker\|all` (180° on alternate rows/columns, head-to-toe ganging), `--step-max=N`. | `rows`, `cols`, `count`, `cell_pt`, `gap_pt`, `sheet_pt`, `grid_pt` |

## 17.2a `--tile=WxH` — wide-format panels *(2.27.0)*

`--tile=WxH in.pdf out.pdf` splits page 1's artboard into panels, one output page per panel. **WxH is the printed panel** (the largest the device prints), and it includes the overlap and any hem. A bare number is in `--unit`, which defaults to inches. The source page is placed once as a form XObject and clipped per panel, so spot colours, overprint and transparency are untouched. The output must be a new file; tiling never overwrites the source.

| Option | Default | Meaning |
|---|---|---|
| `--tile-overlap=N` | 0 | Art shared by neighbouring panels. |
| `--tile-split=even\|fixed` | even | `even`: every panel is the same size. `fixed`: full panels from the start, with the remainder in the last row or column. |
| `--tile-order=rows\|columns` | rows | The panel sequence (output page order). |
| `--tile-corner=tl\|tr\|bl\|br` | tl | Where the sequence starts. |
| `--tile-label=grid\|seq\|off` | grid | The slug label: `A1` (row letter, column number) or the sequence number, followed by `n/N` and the source file name. |
| `--tile-marks=on\|off` | on | Crop marks at every panel corner, plus ticks where the neighbour's art begins (the overlap line) and at hem folds. |
| `--tile-slug=N` | 0.5in (0 when labels and marks are both off) | Margin outside the panel. Marks need 27 pt; with less, there is a warning and no marks. |
| `--tile-hem=N` | 0 | Blank material on the art's outer edges, counted inside WxH. |
| `--tile-grommets=N` | — | Grommet targets along the whole art edge, corners included, no more than N apart. A grommet inside an overlap prints on both panels at the same art position. |
| `--tile-grommet-inset=N` / `--tile-grommet-size=N` | 0.5in / 0.25in | Distance in from the art edge, and the target diameter. These need `--tile-grommets`. |
| `--tile-bleed=N` | 0 | Source art kept past the art's outer edges. There is a warning if that reaches past the source MediaBox. |
| `--tile-page=N`, `--tile-box=crop\|trim\|media\|bleed\|art` | 1, crop | Which page and box are tiled. |
| `--tile-mark-color=TERM` | registration | House colour grammar. |

The output page boxes are as follows. TrimBox is the panel's art plus the hem on outer edges. BleedBox adds `--tile-bleed` on outer edges. MediaBox adds the slug. Panels split the hemmed length (art + 2·hem), then each keeps its share of the art. A page with `/Rotate` is refused (exit 2).

Result: `art_pt [w,h]`, `tile_pt`, `overlap_pt`, `split`, `rows`, `cols`, `count`, `hem_pt`, `bleed_pt`, `slug_pt`, `grommets`, and `panels[]`. Each panel has `{seq, label, row, col, art_pt [x0,y0,x1,y1] in source space, trim_pt, bleed_pt, media_pt, outer[top|right|bottom|left], grommets}`.

Validator: `tools/tile/check_tile.py JAR in.pdf out.pdf result.json [--dpi=N]` checks the geometry (trim ≤ tile, exact overlaps, the art union equals the art box) and re-renders each panel against the source within 1 px. Its positive controls `--corrupt=shift` and `--corrupt=pixel` must FAIL.

## 17.2b `--signature=4|8|16|32` — folded signatures *(2.28.0)*

`--signature=P book.pdf sigs.pdf` imposes reader pages onto folded sheets of P pages: 2×1 cells per side for 4pp, 2×2 for 8pp, 4×2 for 16pp, 4×4 for 32pp. The layout is derived by folding, not from a lookup table. The sheet is folded in half repeatedly, alternating directions and ending with a fold parallel to the page height (4 = V, 8 = HV, 16 = VHV, 32 = HVHV), and each fold takes the right or bottom half under. The last fold is the spine. Each page goes where that folding puts it, rotated 0° or 180°. The back side is the front mirrored left to right (sheetwise back-up). Pages are placed by box as form XObjects and clipped per cell, so colours, overprint and transparency are untouched. Only right-angle power-of-two folds are supported; 12pp and 24pp are not. A bare length is in `--unit`, which defaults to inches.

| Option | Default | Meaning |
|---|---|---|
| `--sig-binding=perfect\|saddle` | perfect | `perfect`: signatures are gathered in order, so signature k holds pages kP+1…(k+1)P. `saddle`: signatures are nested into one stitched section, so the outer signature holds the first P/2 and the last P/2 pages. |
| `--sig-press=sheetwise\|perfecting\|turn\|tumble` | sheetwise | `sheetwise` and `perfecting` give a front page and a back page per signature. `turn` gives one plate twice as wide, with the front on the left and the back on the right (work-and-turn). `tumble` gives one plate twice as tall, with the front at the bottom and the back above it rotated 180° (work-and-tumble). |
| `--sig-gutter=N` | 0.25in | Gap at every fold that is trimmed (heads, feet, faces). The spine fold has no gap. |
| `--sig-grind=N` | 0 | Perfect only: spine milled off each page. The spine gap becomes 2·grind, and page art is kept into it. |
| `--sig-creep=N` | 0 | Saddle only: the innermost sheet's pages shift N toward the spine. The shift falls linearly to 0 at the outermost sheet, counted across all nested signatures. |
| `--sig-bleed=N` | 0 | Page art kept past the trim on edges other than the spine. At most half the gutter. |
| `--sig-margin=N` | 0.5in | Paper outside the page block. Marks need 27 pt; with less, there is a warning and no marks. |
| `--sig-marks=on\|off` | on | Crop marks at trimmed edges and dashed fold marks. For perfect binding, also a collation block on the spine fold between the signature's last and first pages, stepped down by signature number. |
| `--sig-box=trim\|crop`, `--sig-mark-color=TERM` | trim, registration | Which box is the page; the colour uses the house colour grammar. |

Books that are not a multiple of P are padded with blank pages at the end (`blanks`, plus a warning). In saddle binding the padding lands on the last pages of the book. A page of a different size is centred on its cell (warning). A page with `/Rotate` is refused (exit 2). The output must be a new file. Cut-and-stack numbering is not this mode; it is `--pack` `cut_and_stack` (chapter 21, Request and Result).

Result: `pages_in`, `pages_per_signature`, `signatures`, `blanks`, `grid [cols,rows]`, `folds`, `page_pt [w,h]`, `sheet_pt [w,h]`, `binding`, `press`, and `sheets[]`. Each sheet has `{signature, side front|back|turn|tumble, output_page, pages[]}`. Each page entry has `{page (0 = blank), sig_page, col, row, rotate 0|180, trim_pt [x0,y0,x1,y1] on the side's sheet, creep_pt}`. Back-side `col` and `trim_pt` are as seen looking at the back.

Validator: `tools/sig/check_sig.py JAR book.pdf sigs.pdf result.json [--creep=PT] [--dpi=N]` reads only the placement rectangles. It folds each sheet geometrically and checks that the stack reads pages 1…P with heads up and spines on the spine, that rectos back their versos, and that saddle/perfect numbering and creep are right. It then re-renders every cell against the book page within 1 px. Its positive controls `--corrupt=swap`, `--corrupt=rotate` and `--corrupt=pixel` must FAIL. `tools/sig/make_book.py` writes a numbered test book without the jar.

## 17.2c `--compare a.pdf b.pdf` — what changed *(2.29.0)*

`--compare a.pdf b.pdf` pairs pages by number and reports what changed from A to B. It writes nothing except the optional overlay, and it exits 0 whether the files differ or not. Read `identical` for the answer.

- **Plates.** Every Separation/DeviceN ink either file uses, plus Cyan, Magenta, Yellow and Black, is rendered in the renderer's plate mode, so knockouts and overprint resolve as the press sees them. Each file is rendered in one walk per page. A plate pixel is *changed* when its ink differs by more than the threshold. It is *added* where A had no ink on that plate and *removed* where B has none. An ink is listed as in a file when it lays down measured ink, not merely when it is named.
- **Text.** Each page's text is extracted in reading order by position, with runs of white space collapsed. The lines are diffed in order (LCS). Very long pages fall back to an unordered multiset difference, with `method` saying which was used. Up to 200 lines per side are listed; the counts are always complete.
- **Fonts.** Fonts are compared by name with the subset prefix (`ABCDEF+`) removed. A change in embedding is reported as `fonts_changed`.
- **Images, geometry.** For images: the counts and the lowest effective dpi. For geometry: all five page boxes, `/Rotate`, and the union ink bounding box of each file.

| Option | Default | Meaning |
|---|---|---|
| `--compare-pages=1,3-5` | every page | Pages to compare. A page only one file has goes to `pages_only_a` or `pages_only_b`. |
| `--compare-dpi=N` | 150 | Plate render resolution (at most 1200). |
| `--compare-box=crop\|trim\|media\|bleed\|art` | crop | The box rendered; A and B are aligned at its lower-left corner. |
| `--compare-threshold=N` | 32 | Ink levels (0–255) a plate pixel may move and still count as unchanged. |
| `--compare-tolerance=PX` | 0 | A pixel is also unchanged when the other file has that value within PX pixels (0–8). This forgives sub-pixel shifts from re-export. |
| `--compare-text=on\|off` | on | The text diff. |
| `--compare-overlay=FILE.png` | — | The overlay: A's appearance faded, ink removed in red, added in green, changed in blue. With more than one page, `FILE_pN.png` per page. |

Result: `identical`, `pages_a`, `pages_b`, `dpi`, `box`, `threshold`, `tolerance_px`, `inks_a`, `inks_b`, `inks_added`, `inks_removed`, `fonts_added`, `fonts_removed`, `pages_only_a`, `pages_only_b`, and `pages[]`.

Each page has the following fields:
- `{page, identical, changed_px, added_px, removed_px, changed_pct, bbox_pt, raster_px [w,h]}`.
- `plates[]` lists each inked plate as `{name, kind spot|process, in_a, in_b, changed_px, added_px, removed_px, changed_pct, bbox_pt}`.
- `text {changed, lines_a, lines_b, method, removed_count, added_count, removed[], added[]}`.
- `fonts_added`, `fonts_removed` and `fonts_changed[] {name, a, b}`.
- `images {a, b, min_dpi_a, min_dpi_b}`.
- `geometry {a{box: [x0,y0,x1,y1]}, b{…}, rotate_a, rotate_b, changed[], ink_bbox_a, ink_bbox_b}`.
- `overlay`.

Bounding boxes are in points in A's page space; a page with `/Rotate` gives a warning and uses the rendered frame. Page-level pixel counts are the union over all plates: a pixel is added when no plate had ink there in A, and removed when no plate has ink there in B.

Validator: `tools/compare/check_compare.py JAR WORKDIR` writes a hand-built A/B pair whose changes are on whole points. It asserts exact counts at 72 dpi: a moved square, an added spot, a text and font change, a TrimBox change, an extra page, and the overlay colour counts. It also checks that a self-compare is identical and that `--compare-tolerance=1` forgives a 1 pt shift that tolerance 0 reports.

## 17.3 Marks and keyline

**`--marks[=crop,reg,center,bleed|all]`** — the artboard becomes the TrimBox/ArtBox and the page
grows around it to hold corner crop marks and side registration targets, drawn in the
registration colour (Separation `/All`, every plate) unless `--marks-color=spot(..)|cmyk(..)`.
Defaults: 0.125 in offset, 0.25 in marks, 0.25 pt weight, 0.25 in targets. `--bleed=N` records a
BleedBox and pushes the marks beyond it; the art does not move. Shop parameters: `--marks-box=`,
`--marks-reg-pos=sides|corners|all`, `--marks-reg-offset=N`, `--marks-slug=N` (extra growth for
labels), `--marks-cap=butt|round|square`, `--marks-offset`, `--marks-length`, `--marks-weight`,
`--marks-reg-size`. Result: `trim_pt`, `bleed_pt`, `media_pt`, `marks[]`, `colour`, `weight_pt`,
`reg_size_pt`, `pages`.

**`--keyline[=spot('Name',c,m,y,k)]`** — a dieline rectangle on top of the art in its own spot
plate (default `spot('Dieline',0,100,0,0)`), overprinting so it never knocks ink out.
`--keyline-box=artboard|ink|bleed|trim`, `--keyline-inset`, `--keyline-weight`, `--keyline-radius`,
`--keyline-dash=A,B[unit]`, `--keyline-knockout`, `--keyline-align=center|inside|outside`,
`--keyline-join`, `--keyline-cap`, `--keyline-opm=0|1`, `--keyline-below`. Result: `keyline_pt`,
`colour`, `weight_pt`, `dash_pt|null`, `overprint`, `align`, `pages`.

## 17.4 `--edge` — a band around the solid mass

```
java -jar java-epic-cli.jar --edge="spot('PANTONE White',0,0,0,0)" --edge-width=3pt in.pdf out.pdf
```

Jesse's example: "a white stroke on everything 3pt, then convert all strokes into path fills,
then refit to bounds" — which is an **edge around the solid mass of the combined objects, not a
stroke per object**. The union of every painted object (fills, strokes, text) is lifted exactly
(raster images are **not** part of the mass; a warning counts them, and its wording "contribute
their full frame" is wrong but kept so existing output stays byte-identical. `--cut-contour`
traces rasters), grown outward by the width (Offset Path: miter join, limit
4 by default) and the ring between is filled in the spot. The art is untouched; the ring goes
**under** it (`--edge-above` paints on top). Every page box grows by the width, art fixed, so
Illustrator's artboard record follows (`--edge-keep-artboard` leaves the boxes). Measured against
Jesse's hand-made Illustrator file: ink IoU 1.000, white IoU 0.992.

Shop parameters: `--edge-join=miter|round|bevel`, `--edge-miter=N`, `--edge-tint=N` (percent of
the spot), `--edge-grow=N` (how far the boxes grow; 0 = not at all), `--edge-threshold=N` (opacity
percent below which paint is not mass), `--edge-ignore-white` (process white and 0% tints are not
mass; a named White ink is). Result: `colour`, `width_pt`, `join`, `miter_limit`, `tint`,
`band_area_pt2`, `mass_bbox_pt`, `artboard_pt`.

## 17.4a `--cut-contour[=OFFSET]` and `--export-cut=dxf|svg|pdf` — print-and-cut *(2.15.0)*

```
java -jar java-epic-cli.jar --cut-contour=0.0625 sticker.pdf sticker_cut.pdf
java -jar java-epic-cli.jar --export-cut=dxf --cut-unit=mm sheet.pdf sheet.dxf
```

**`--cut-contour`** draws the cut line a print-and-cut RIP reads, such as VersaWorks, Onyx, Caldera,
Summa GoProduce or Zünd Cut Center. It starts from the page's whole-ink footprint, the same one
`--backers` grows its union plates from: vector paint is lifted exactly, and placed rasters and
shadings are traced at their alpha (`--cut-threshold`, default 50%). A cut line and a backer
built from the same art therefore follow the same edge. The footprint is offset by OFFSET (Offset
Path; a bare number means `--unit`, default inches; negative cuts inside the art; default 0 follows
it) and **stroked** on top of the art in its own spot, overprinting and never filled. The defaults
are `spot('CutContour',0,100,0,0)`, 0.25pt and round joins. Only the outer silhouette of each
piece is cut, because a sticker is not cut along its counters; `--cut-holes` cuts the inner
contours too. Pieces close enough to touch after the offset merge into one line. The page boxes
grow (uniformly, art fixed) only by as much as the line overhangs the artboard. If the art
already has a plate of that name, a warning says so and the new line is added beside it.

Shop parameters: `--cut-color=spot(..)`, `--cut-weight=PT`, `--cut-join=round|miter|bevel`,
`--cut-miter=N`, `--cut-min-size=N` (drop pieces smaller than N in both directions, e.g. dust),
`--cut-threshold=N`, `--cut-dpi=N` (raster trace resolution, default 300), `--cut-keep-artboard`,
`--cut-knockout`. Result: `colour`, `offset_pt`, `weight_pt`, `join`, `miter_limit`, `holes`,
`overprint`, `pages`, `paths`, `length_pt`, `cut_bbox_pt`, `artboard_pt`, `traced` (some art went
through the raster trace).

**`--export-cut`** writes the cut lines of a print file as a cutter file. It takes every path
**stroked** in a cut spot, lifted in page space through nested forms, so a `--cut-contour`, a
`--keyline` and a `--step` or `--imposition` sheet of either all export the same way. A 3×2 step
of a two-piece contour exports 12 paths at exactly six times the single length. `--cut-spots=A,B`
names the spots (default CutContour). Names match ignoring case, spaces, hyphens and underscores,
so `Cut Contour` matches. Fills in a cut spot are not cut lines; they are counted in
`fills_ignored` and left out. No cut path at all is exit 1.

| format | what it holds |
|---|---|
| `dxf` | AutoCAD R12 ASCII. One `POLYLINE` per cut path, closed flag set when it closes, layer = spot name, `$INSUNITS` set. R12 has no curves, so beziers are flattened to `--flatness` (default 0.01mm). Checked with ezdxf: 0 audit errors, length within 0.03%. |
| `svg` | One `<path>` per cut, curves kept, Y down, `width`/`height` in the unit, viewBox in the unit. |
| `pdf` | The cut lines alone, one page per source page, source boxes kept, 0.25pt strokes in each spot. |

DXF and SVG hold one page (`--page=N`, default 1, with a warning when there are more). The unit is
`--cut-unit=mm|cm|in|pt`, or `--unit` when that is given, otherwise mm. The origin is
`--origin=artboard|media|trim|cut` (default: the artboard's lower-left; `cut` = the lines' own
bbox). Output defaults to `<source>.dxf` / `.svg` / `-cut.pdf`. Result: `format`, `unit`,
`origin`, `spots` (as the file spells them), `pages`, `paths`, `open_paths`, `length_pt`, `bbox`
(in the output unit and origin), `fills_ignored`.

**`--cut-marks[=4|3]`** *(2.16.0)* prints camera registration marks around the cut lines (the
paths `--export-cut` would lift, so run `--cut-contour` or `--keyline` first). They are filled
squares in their own spot, default `spot('Regmark',0,0,0,100)`, which prints as solid black.
Each mark is 5mm square (`--cut-mark-size`), 5mm clear of the cut box (`--cut-mark-gap`). `4`
(the default) puts one at every corner; `3` leaves out the top-right one so the camera can tell
which way the sheet is fed. `--cut-mark-every=N` adds marks along each side so no two are more
than N apart, for long sheets on roll cutters. `--cut-mark-color` picks another plate, and
`--cut-spots` says what counts as a cut line. The boxes grow by the overhang unless
`--cut-keep-artboard`. A page with no cut lines gets a warning and no marks, and no cut lines on
any page is exit 1. These are generic square marks. Whether a given cutter accepts them, at what
size and distance, is that cutter's own setting, and no vendor mark format is claimed; every value
is an overridable default. Result: `colour`, `count`,
`size_pt`, `gap_pt`, `pages`, `marks[{page, x_pt, y_pt}]` (centres in page space),
`artboard_pt`.

*(2.17.0)* `--cut-mark-margin=N` keeps N clear beyond each mark when the boxes grow, so a
trimmer or RIP clip does not take part of a mark. The default is 0 (the 2.16 flush placement,
byte-identical), or the gap when `--cutter` is given. `--cut-mark-shape=square|circle` sets the shape. `--cutter=NAME` sets shape,
size and gap from a vendor's published documents; any explicit `--cut-mark-*` flag still wins:

| `--cutter` | shape | size | gap | published source |
|---|---|---|---|---|
| `summa-opos` (`summa`, `opos`) | square | 3 mm | 9 mm | Summa DC5 manual p.52: square, advised 3 mm, white space 3–4× the mark size |
| `zund` | circle | 6 mm | 5 mm | Zünd OCC datasheet: recommended 6–12 mm; ZCC 3.3 notes: any shape since ZCC 2.4 |
| `kongsberg` (`esko`, `i-cut`) | circle | 5 mm | 5 mm | Esko iPC 2.0 manual §6.14.2: circle 3–12 mm recommended, clear mask D ≥ d + 4 mm |

When any of these three options is given, the result adds `shape`, `margin_pt` and `cutter`.
Without them, the JSON is unchanged from 2.16. There is no Graphtec preset: ARMS mark
geometry is published only as figures, so `--cutter=graphtec` is exit 1 with that reason; give
the `--cut-mark-*` values from the machine's own manual. No vendor documents a required mark
spot name; `Regmark` is the house default. Zünd and Esko read mark positions from the cut file,
which is what `--export-cut` marks[] and its Regmark layer carry.

In one call with `--cut-contour` (either order, 2.16.1) the marks go around that run's own line
(its `--cut-color` spot unless `--cut-spots` says otherwise). Every `--cut-mark-*` option is checked
before anything is written, and the contour result gains `marks` and `marks_colour`. Through
2.16.0 that combination silently dropped the marks.

`--export-cut` carries the marks too. Fills in `--mark-spots` (default `Regmark`) go out as closed
outlines on their own layer (DXF) or `<g>` (SVG, filled black), and are filled in their spot in
the PDF. Their centres appear in `marks[[x, y]]`, in the output unit and origin. With no marks the
field is absent and the output is byte-identical to 2.15.0.

Not yet: ZCC and Kongsberg JDF job files. They need public format documents first. The mark
presets are `--cutter` above.

## 17.4c `--barcode=SYM:DATA` — vector barcodes *(2.18.0)*

```bash
java -jar java-epic-cli.jar --barcode=ean13:590123412345 ean.pdf
java -jar java-epic-cli.jar "--barcode=gs1-128:(01)09506000134352(17)201225(10)AB12" --x-dim-mm=0.495 g.pdf
java -jar java-epic-cli.jar --barcode=gs1-dl-qr:https://id.gs1.org/01/09506000134352 --barcode-color="spot('Barcode',0,0,0,100)" q.pdf
```

The output is one page whose boxes are the symbol, quiet zones included, so placing the page
reserves the quiet zone. Everything is filled vector paths in one colour, default
`cmyk(0,0,0,100)` (one plate). The human-readable text is outlined from the bundled Liberation
Sans, so no font is embedded or substituted. ZXing core 3.5.3 (Apache 2.0) encodes the symbols.
Check digits, GS1 element strings and the layout are this tool's own.

| SYM | DATA | check digit | quiet zone (X) | default X / bar height |
|---|---|---|---|---|
| `ean13` | 12 digits, or 13 | computed / verified | 11 left, 7 right | 0.330 mm (100%) / 22.85 mm |
| `ean8` | 7, or 8 | computed / verified | 7 / 7 | 0.330 mm / 18.23 mm |
| `upca` | 11, or 12 | computed / verified | 9 / 9 | 0.330 mm / 22.85 mm |
| `upce` | 6 (number system 0), 7, or 8 | of the expanded UPC-A | 9 / 7 | 0.330 mm / 22.85 mm |
| `itf14` | 13, or 14 | computed / verified | 10 / 10 | 0.635 mm / 32 mm |
| `code128` | ASCII | (symbol checksum only) | 10 / 10 | 0.330 mm / 15 mm |
| `gs1-128` | `(AI)value...` | GTIN/SSCC/GLN | 10 / 10 | 0.330 mm / 15 mm |
| `qr` | any text (UTF-8) | — | 4 | 0.5 mm; `--ecc=L|M|Q|H`, default M |
| `datamatrix` | any text | — | 1 | 0.5 mm, square symbols |
| `gs1-datamatrix` | `(AI)value...` | GTIN/SSCC/GLN | 1 | 0.5 mm |
| `gs1-dl-qr` | `http(s)://host/.../01/<GTIN>...` | GTIN/SSCC/GLN in the path | 4 | 0.5 mm |

A supplied check digit that is wrong is exit 1, and the reason names the right digit. EAN/UPC
heights scale with the X dimension (GS1 100% = 0.330 mm). The report gives `magnification_pct`
and warns outside 80–200%, or when `--height-mm` truncates the bars. The other defaults are
house defaults, not standards claims; set `--x-dim-mm` and `--height-mm` to your spec.

GS1 data is bracketed: `(01)09506000134352(17)201225(10)AB12`.

- FNC1 leads the symbol and follows every element except the last, unless its AI is one of the
  predefined-length prefixes (00, 01–04, 11–20, 31–36, 41).
- Those elements have their exact length enforced, because nothing else would stop them running
  into the next AI.
- GTIN (01, 02), SSCC (00) and GLN (410–417) check digits are computed or verified, and dates
  (11–13, 15–17) must be YYMMDD.
- Every value must be in GS1 character set 82, at most 90 characters.
- Other AIs are not checked against the full GS1 AI table.
- A value containing `(` cannot be given, because the bracketed form cannot tell it from the
  next AI.

Verified by decoding renders: GS1-128 reads as `]C1` and GS1 DataMatrix as `]d2`.

`--bwr-mm` is bar width reduction, 0 ≤ BWR < X:

- 1D bars are drawn BWR narrower, centred on their nominal positions.
- 2D modules are traced as one exact union of grid polygons, with every edge moved inward by
  BWR/2. Modules touching only at a corner stay separate shapes.

Other options:

- `--quiet=N` sets both quiet zones in modules, with a warning below the minimum.
- `--hri=on|off` controls the text; the default is on for 1D, off for 2D.

Result:

```json
{"status":"ok","output":"/abs/ean.pdf","symbology":"ean13","input":"590123412345",
 "data":"5901234123457","check_digit":7,"hri":"5901234123457","x_dim_mm":0.33,
 "magnification_pct":100.0,"bwr_mm":0.0,"bar_height_mm":22.85,"modules":[95],
 "quiet_zone_modules":[11,7],"size_mm":[37.29,25.562],"colour":"cmyk(0,0,0,100)","warnings":[]}
```

`modules` is `[bars]` for 1D and `[columns, rows]` for 2D. `ecc` is present for QR only.

This is a prepress check, **not** an ISO/IEC 15415/15416 print-quality grade: that needs a
verifier reading the printed symbol. There are no ITF-14 bearer bars yet.

On an imposition sheet, the same symbol is the generated mark
`@barcode:SYM[;key=value..]:DATA` (chapter 6).

## 17.4d `--barcode-check=FILE` — read and measure the barcodes in a PDF *(2.19.0)*

```bash
java -jar java-epic-cli.jar --barcode-check=label.pdf
java -jar java-epic-cli.jar --barcode-check=sheet.pdf --page=2 --dpi=1200
```

Each page is rendered at `--dpi` (default 600, range 150–2400). A page that would go over
40 megapixels is rendered at a lower dpi and says so in `warnings`. The page is padded with
white so a symbol at the page edge is still found. ZXing reads every symbol on the page, then
each compact dark blob (3–80 mm) is read again on its own for Data Matrix, QR and Aztec, because
ZXing's whole-page Data Matrix search misses a symbol among other art. Since 2.20.0 the page is
also read turned 90° for 1D symbols, so a symbol whose bars run across the page (an EAN on its
side) is found and measured; 2.19.0 did not find those. The same symbol read twice is reported
once. Since 2.21.0 a 1D read must also decode to the same text on at least two of three rows
through its bars (20%, 50%, 80% of the shortest bar), or it is dropped: ZXing accepts the
first row that decodes, and a line of large type could pass as UPC-E (2.19.0 and 2.20.0
reported a 16 pt name line as `upce 11225629`).

Geometry is measured along the page axes. A 1D symbol is measured at 0°, 90°, 180° or 270°. A 1D
symbol at another angle is usually not found at all. A 2D symbol turned by any other angle is
still read, but `x_dim_mm` and the quiet zones are left out and a warning gives the angle
(2.19.0 reported wrong values for these).

For each symbol:

| field | meaning |
|---|---|
| `symbology` | `ean13`, `ean8`, `upca`, `upce`, `itf14`, `itf`, `code128`, `gs1-128`, `qr`, `gs1-qr`, `gs1-dl-qr`, `datamatrix`, `gs1-datamatrix`; any other format is lower-cased and only read |
| `format`, `aim` | ZXing's format, and the AIM symbology identifier (`]C1` = GS1-128, `]d2` = GS1 DM, `]Q3` = GS1 QR) |
| `data` | the decoded text; GS (U+001D) is the FNC1 separator, and the leading `]C1`/GS is removed |
| `gs1[]` | GS1 symbols only: elements with a predefined-length AI are cut at their fixed length and checked (`ai`, `value`, or `error`); any other element runs to the next GS and has `raw` only, because its AI length needs the full GS1 table |
| `check_digit`, `check_digit_ok` | EAN/UPC/ITF-14 (and UPC-E via its UPC-A expansion) |
| `x_dim_mm`, `modules` | the module width measured from the render; bars for 1D, `[columns, rows]` for 2D |
| `magnification_pct` | EAN/UPC; outside 80–200% is a problem |
| `wide_ratio` | ITF; a warning outside 2.25–3.0 |
| `bar_height_mm` | 1D, the shortest bar |
| `bbox_mm` | `[x0, y0, x1, y1]` on the crop box, y up |
| `quiet_zone_modules`, `quiet_zone_required` | `[left, right]` for 1D, `[left, top, right, bottom]` for 2D. EAN-13 11/7, EAN-8 7/7, UPC-A 9/9, UPC-E 9/7, other 1D 10/10, QR 4, Data Matrix 1. Short by more than ¼ X is a problem, marked "(cut by the page edge)" when the page ends first |
| `ink[]`, `background[]` | the plates under the dark and the light modules (`plate`, `coverage_pct`, kept at 0.5% and over). Antialiased module edges are ignored |
| `split` | the dark modules are on more than one plate (rich black, registration colour); also a warning, because misregistration changes the bar widths |
| `problems[]`, `warnings[]` | problems fail the symbol; warnings do not |

The result also has `dpi`, `pages`, and `ok`. `ok` is true when at least one symbol was found and
none has a problem. A page with no symbol gets the warning "no barcode found". The exit code is
0 whenever the check ran; a check that finds problems is still exit 0 with `ok:false`. A file
that is not a readable PDF is exit 2. A missing file, a `--page` past the end, or a bad option
is exit 1.

```json
{"dpi":600.0,"pages":1,"ok":true,"barcodes":[{"page":1,"symbology":"gs1-128","format":"CODE_128",
 "aim":"]C1","data":"01095060001343521720122510AB12\u001d21XYZ",
 "gs1":[{"ai":"01","value":"09506000134352","raw":"0109506000134352"},
        {"ai":"17","value":"201225","raw":"17201225"},{"raw":"10AB12"},{"raw":"21XYZ"}],
 "x_dim_mm":0.33,"modules":[310],"bar_height_mm":14.99,"bbox_mm":[3.3,3.56,105.58,18.54],
 "quiet_zone_modules":[10.0,10.0],"quiet_zone_required":[10,10],
 "ink":[{"plate":"Black","coverage_pct":100.0}],"background":[],"split":false,
 "problems":[],"warnings":[]}],"status":"ok","source":"/abs/g128.pdf","warnings":[]}
```

Every value is measured from a render, so it is only as accurate as the render; 600 dpi resolves
X to about 0.04 mm. This is a prepress check of the file, **not** an ISO/IEC 15415/15416 grade:
that needs a verifier reading the printed symbol.

## 17.4b `--trap=W[unit][:MODE]` — vector trapping *(2.8.0)*

```
java -jar java-epic-cli.jar --trap=0.25pt in.pdf out.pdf
java -jar java-epic-cli.jar --trap=0.3:centre --trap-pairs="PANTONE 123 C>Process" in.pdf out.pdf
```

Trapping on the composite PDF, for a "trap" step in a prepress action chain (web2print asked,
2026-10-23). `--separate --trap=N` is a different thing: the raster plate option in source pixels,
untouched; `--trap` is this mode only when `--separate`/`--ink-coverage` is not on the line.

- **Inks and what is visible.** The page is walked once in paint order (forms included) and every
  ink's visible region is kept as an exact Area: a paint adds its footprint to its own ink and,
  unless it overprints, knocks it out of every other ink. An ink is one paint colour: a spot at a
  tint, a DeviceN mix, a process colour. Images, shadings, patterns, soft-masked paint, process
  white, backer spots (adhesive, underbase, clear, varnish, glue) and registration knock out but
  are never inks and never trapped against (warned).
- **Which way.** Where two inks meet one grows into the other by `W` (bare number = pt; at most
  10pt): `spread` [default] the lighter grows under the darker, `choke` the darker grows over the
  lighter, `centre` `W/2` each — the same words as `--separate --trap`. Lighter = higher luminance
  as painted; equal luminance: the ink painted first is the darker. `--trap-pairs="A>B;.."` traps
  only the named pairs, A growing under B; a name is a spot name, an ink label from the result, or
  `Process` (any process paint).
- **Not trapped:** two inks sharing a plate — the same spot at two tints, or process colours with a
  plate at 10% or more in both — because a misregistration there shows that plate, not paper. An
  edge of one ink deep inside an overprinting other needs no trap either and gets none.
- **White is an ink** (house rule). A named White is the lightest ink, so it spreads under every
  neighbour; name the pairs to keep it out.
- **Output.** Each trap is a filled path in the growing ink's own colour space and value,
  overprinting (`OP`/`op` true, `OPM 1`), drawn after the art; the original content is wrapped in
  `q`..`Q` so the traps land in default user space. Proven on plates: separating the trapped file
  with `--separate`, only the growing plate changes (a 4pt spread grows Yellow exactly 8px at 144
  dpi into both neighbours; the neighbours' plates keep the same coverage and edges).
- **Result:** `width_pt`, `mode`, `join` (`--trap-join=round|miter|bevel`, default round),
  `trap_count`, `trap_area_pt2`, `traps[{page, under, over, area_pt2}]` (`under` = the growing
  ink), `inks[{page, ink, luminance, visible_pt2}]`. No abutting inks → the source re-saved,
  warned.
- **Cost.** Exact Area geometry: under a second on simple art; a dense logo (tanimura, three
  spots, 16k content lines) ~11 s at 0.25pt. `-Dtrap.debug=1` prints the phases to stderr.

## 17.5 `--expand-strokes` and `--outline-text`

**`--expand-strokes`** — every stroked path (pages, forms, patterns) becomes the filled outline of
its stroke in the stroking colour: width, cap, join, miter and dash honoured in **user space**, so
a scaled CTM distorts the outline exactly as it distorted the stroke. `B`/`b` paint fill then
outline; a `W` clip is applied after. Zero-width hairlines expand at `--hairline=PT` (default
0.1, warned). `--expand-min-width` / `--expand-max-width` expand only that range (the rest stay
strokes, counted in `strokes_kept`); `--expand-join`, `--expand-cap`, `--expand-miter` override
the source's own treatment (default: the source's, as Outline Stroke does). Result:
`strokes_expanded`, `zero_width`, `hairline_pt`.

**`--outline-text`** — every text run (pages, forms, coloured patterns) becomes paths in the same
place, colour, overprint and clip. Fill, stroke, fill+stroke and text-clip render modes are
honoured; invisible text is dropped. Type3 fonts and vertical writing stay as text (warned). A
non-embedded font is outlined from the substitute face (warned) — `--outline-missing=substitute|
keep|error`. Unreferenced font resources are removed. TrueType outlines are rescaled by
1000/unitsPerEm; quadratics are elevated exactly to cubics. Result: `glyphs_outlined`,
`text_runs_kept`, `fonts_removed`, `pages`.

## 17.6 Colour conversion and overprint

| Flag | What it does | Result |
|---|---|---|
| `--spot-to-process[=Name;Name]` | Separation/DeviceN fills and strokes repainted in DeviceCMYK through the file's **own tint transform**, tint honoured; with names, only those. Images and shadings are counted and warned, not re-encoded. Converted colour-space resources are removed so the plate leaves `--list-spot-colors`. | `converted[{name,cmyk_pct}]`, `ops_rewritten`, `images_untouched`, `shadings_untouched` |
| `--convert=cmyk` | DeviceRGB / ICC-RGB / CalRGB fills and strokes become DeviceCMYK by the plain conversion (K = 1 − max) — the exact round trip with `--quantize` and `--separate`. | same rule |
| `--convert=cmyk --profile=[SRC.icc,]DST.icc [--intent=perceptual\|relcol\|sat\|abs]` *(2.22.0)* | Converts through ICC profiles instead of the plain rule. DeviceRGB goes through SRC (default: the JDK's built-in sRGB); ICCBased RGB goes through **its own embedded profile**; CalRGB, and spot alternates that are RGB or Lab, go through their sRGB appearance. Everything lands in the CMYK profile DST. Spot alternates that are already CMYK are kept. Default intent relcol. No black point compensation: the JDK engine never applies it, so `--bpc` is refused (exit 1). Without `--profile` nothing changes. Wrong profile type, a missing file or a non-ICC file exits 1. Needs `java -jar`, because the manifest's `Add-Exports` opens the JDK colour engine. | `profile{source, destination, intent, embedded_profiles}` |
| both | `--tac=N` caps total area coverage at N percent (100–400): CMY scaled down together, K kept. | `tac_clamped` |
| `--output-intent=FILE.icc[,id=ID][,info=TEXT][,condition=TEXT][,registry=NAME][,subtype=pdfx\|pdfa\|pdfe]` *(2.23.0)* | Sets the catalog `/OutputIntents` entry, with FILE.icc embedded **byte for byte** as `/DestOutputProfile` (the profile must be CMYK, RGB or gray). The subtype is written as `/S`: `pdfx` = `GTS_PDFX` (default), `pdfa` = `GTS_PDFA1`, `pdfe` = `ISO_PDFE1`. An existing intent of the same subtype is replaced; other subtypes are kept. `id` (OutputConditionIdentifier) and `info` default to the profile's description tag; `condition` and `registry` are written only when given. A comma splits only before a known `key=`, so values can contain commas. `--output-intent=remove[,subtype=…\|all]` removes intents. Content colours are not changed; use `--convert=cmyk --profile` for that. Given together with `--convert=cmyk` or `--spot-to-process` (in either flag order), the intent is written in the conversion's own save, and that result adds `output_intent` (or `output_intents_removed`). Bad profile or subtype: exit 1, and nothing is written. | `output_intent{subtype, identifier, info, condition, registry, profile, profile_description, color_space, profile_class, components}`, `replaced` / `removed` |
| `--pdfx-check=x1a\|x4 <src>` / `--pdfx-check=x1a\|x4 --pdfx-fix [--trapped=true\|false] [--output-intent=FILE.icc[,…]] <src> [out]` *(2.24.0)* | A **house preflight** against the published PDF/X-1a:2003 / PDF/X-4 requirement list, rule by rule. It is **not** an ISO 15930 conformance certificate: it checks the rules below and nothing else, and every result says so in `basis`. Rules: `encrypted`; `output_intent` (exactly one `GTS_PDFX` intent with an OutputConditionIdentifier; a warning when it has no DestOutputProfile; x1a: not an RGB profile); `page_boxes` (TrimBox or ArtBox on every page; Trim inside Bleed inside Media, read from the raw arrays because PDFBox clips them); `fonts` (embedded); `trapped` (Info `/Trapped` True or False); `version_key` (x1a: Info `GTS_PDFXVersion` `PDF/X-1a:2003`, or `PDF/X-1:2001` with conformance `PDF/X-1a:2001`; x4: XMP `pdfxid:GTS_PDFXVersion` `PDF/X-4`); `pdf_version` (header at most 1.4 for x1a, 1.6 for x4); `javascript`; `color` (x1a allows only DeviceCMYK, DeviceGray, and Separation/DeviceN/Indexed over them; x4 flags DeviceRGB/DeviceCMYK that differ from the intent's family and are not covered by DefaultRGB/DefaultCMYK); x1a only: `transparency` (soft masks, alpha below 1, blend modes other than Normal/Compatible, transparency groups), `optional_content`, `jpx`; `annotations` (a warning for anything other than PrinterMark/TrapNet). Forms, patterns, Type3 fonts and inline images are walked. **`--pdfx-fix`** changes only these things: it sets TrimBox = CropBox where a page has neither TrimBox nor ArtBox; sets Trapped from `--trapped` (never guessed); sets the intent from `--output-intent`; lowers the header version unless optional content or JPEG 2000 need it; and, LAST, writes the version key, only when no other error is left. An XMP that names another PDF/X version is left as it is, with a warning. The fix then re-checks. Any other mode's flag is refused (exit 1), so run that step first. Exit 0 whenever the check ran, whether or not it passed; 2 for an unreadable PDF; 1 for usage. `--pdfx-check` alone never writes. | `standard`, `basis`, `pass`, `errors`, `warnings_count`, `issues[{rule, severity, page, detail, fixable}]`, `fixed[]` (fix only), `pages` |
| `--overprint=on\|off [--opm=0\|1]` | Sets OP/op (and OPM, default 1 when on) in every ExtGState and at the top of every page, so every object overprints (or knocks out). `--overprint-part=fill\|stroke\|both`, `--overprint-pages=`. | `overprint`, `opm`, `ext_g_states_patched`, `part` |
| `--ink-coverage <src.pdf>` | `--separate`'s `pages[].plates[{name,kind,coverage_pct,blank}]` with every `file` null: plates are rendered and measured, nothing written. `--dpi`, `--page`, `--plates`. Raster input refused (exit 1). | as `--separate` |
| `--tac-check[=N]` *(1.43.0)* | With `--ink-coverage` or `--separate`: every non-blank plate's ink summed per pixel against N percent total area coverage (`--tac`, else 300). Measured after `--trap`, so grown ink counts. A page's plates must share a grid. | per page `tac{limit_pct, max_pct, mean_pct (over inked pixels), over_pct (share of the page), over_pixels, inked_pixels, pixels, plates[]}`; top-level `tac_max_pct`, `tac_over`; a warning per page over the limit |

## 17.6a `--soft-proof=FILE.icc|intent` — soft proof on `--render` *(2.25.0)*

```
java -jar java-epic-cli.jar --render=150 --soft-proof=ISOcoated_v2.icc in.pdf proof.png
java -jar java-epic-cli.jar --render=150 --soft-proof=intent --soft-proof-paper=on in.pdf proof.png
```

The page renders as it would print on a CMYK press profile, shown in sRGB. `intent` uses the
document's own output intent (`GTS_PDFX` first, otherwise the first intent that embeds a
`DestOutputProfile`). This is not `--separate --proof`, which composites the emitted plates.

| Colour | How it is proofed |
|---|---|
| DeviceCMYK | Taken as the press's own numbers: press profile to the display only. |
| DeviceGray | The press's K. |
| ICCBased | Its own profile to the press at the intent, then to the display. A profile byte-identical to the press profile counts as the press itself. |
| DeviceRGB, CalRGB, CalGray, Lab | Their usual sRGB appearance, then to the press at the intent, then to the display. |
| Separation, DeviceN, Indexed, patterns | Through the spaces above, so a spot proofs through its alternate. |
| Soft masks, image masks | These are shape, so they are never proofed. |

- `--soft-proof-intent=perceptual|relcol|sat|abs` sets how non-press colour reaches the press. The default is relcol.
- `--soft-proof-paper=on|off` defaults to off.
  - Off: the display stage is relative colorimetric, so the paper shows as display white.
  - On: the display stage is absolute, so the paper shows as its real colour. The render is then matted with it, unless `--render-matte` is given; JPEG then uses the paper, not white.
- The result adds `soft_proof{profile ("document" or the absolute file path), profile_description, intent, paper, paper_color}`.
- Errors and exit codes:
  - Exit 1, nothing written: a bad intent or on/off value, a missing or non-ICC file, a non-CMYK profile, or `-intent`/`-paper` without `--soft-proof`.
  - Exit 2: `--soft-proof=intent` on a document with no usable CMYK intent profile.
- Plate renders (`--separate`) ignore it.
- Without `--soft-proof`, every render is byte-identical to 2.24.0.
- All transforms use the JDK's LittleCMS (16-bit, no black point compensation).
- Measured on a swatch file against `transicc` 2.09, chaining the same stages: every swatch was exact (0/255) at relcol, perceptual and paper-on.

`--derivative-preview` takes the same three flags (chapter 12). There the derivative's own
intent is read, and the PNG stays unmatted.

## 17.6b `--distort=cone:TOP,BOTTOM,HEIGHT[unit]` *(1.44.0)*

Pre-distortion for a tapered vessel (cup, tumbler, bucket, lamp shade): the flat art is bent into
the annular sector that wraps the cone, so that printed flat and wrapped it reads straight. TOP and
BOTTOM are the vessel's **diameters** at the top and bottom of the printed band, HEIGHT the band's
vertical height (bare lengths in `--unit`, default in; a suffix on the last value sets all three).
They set the **curvature only** — the art keeps its own size: its height runs along the slant, its
width around the vessel at the reference edge (`--distort-ref=top|middle|bottom`, default top, so
the width is true at the top edge). A warning says when the art's height and the band's slant
height differ by more than 1%. Equal diameters are a cylinder: the file is copied with a warning.
Which end is wider decides which way the fan bends.

`--distort=shrink:X%,Y%` is the affine kind and belongs to the art transforms, [§5.1b](05-transform-modes.md).

**PDF** (`[output]` optional, source overwritten when omitted). Text is outlined and strokes are
expanded first, in-process, with exactly the `--outline-text` and `--expand-strokes` code (so
`--strict-fonts` and `--fonts-dir` apply). Then every fill and clip, on the page and inside the
forms it draws, is flattened in page space, cut into pieces short enough to stay within 0.005 pt of
the true curve (`segment_pt`) and mapped point by point; every `cm` is absorbed, so the output
draws under an identity CTM. Form XObjects drawn once are rewritten in place with their matrix
reset. The page boxes become the bounds of the bent artboard (`--box`, media by default).
Illustrator private data is removed (no live document describes bent art).

Refused with exit 1 and a counted reason: images, inline images, shadings, text that could not be
outlined, a form drawn more than once at different places. Rasterize such a page (`--render`) and
bend the picture instead. Tiling pattern fills keep their flat tiling (their outlines are bent,
counted in `patterns_untouched`, warned). A stroke that `--expand-strokes` left alone is bent as a
stroke and warned.

**Picture** (png/tif/jpg, output required): every output pixel is mapped back through the inverse
law and sampled bilinearly (premultiplied); `--dpi=N` (300) gives the picture its size in points.
Output is an RGBA PNG; the fan's corners are transparent.

| Result field | Meaning |
|---|---|
| `distort{kind, top_pt, bottom_pt, height_pt, slant_pt, ref, wrap_deg, radius_bottom_pt, radius_top_pt, segment_pt}` | the law: diameters and height in points, slant height, reference edge, degrees the art spans at that edge (warned above 360), development radii of the art's bottom and top edges, longest straight piece |
| `artboard_before_pt`, `artboard_pt` | flat artboard and the bent bounds (page 1) |
| `paths_warped`, `points` | paths bent (clips excluded), points emitted |
| `forms_rewritten`, `patterns_untouched`, `glyphs_outlined`, `strokes_expanded`, `pages` | PDF only |
| `width_px`, `height_px`, `dpi` | picture only |

```
java -jar java-epic-cli.jar --distort=cone:3.5,2.5,4 label.pdf label_cone.pdf
java -jar java-epic-cli.jar --distort=cone:90mm,60mm,100mm --distort-ref=middle label.pdf
java -jar java-epic-cli.jar --distort=cone:3.5,2.5,4 --dpi=300 label.png label_cone.png
```

## 17.6c `--wrap <image>` — a picture as a one-page PDF *(2.32.0)*

Turns one PNG, JPEG or TIFF into a one-page PDF exactly the size of the picture, so the PDF prepress tools can take a raster. The page is `pixels × 72 / DPI` points; nothing is resampled, recoloured or traced.

```
java -jar java-epic-cli.jar --wrap art.png --output=art.pdf
java -jar java-epic-cli.jar --wrap scan.tif --output=scan.pdf --dpi=600
java -jar java-epic-cli.jar --wrap=photo.jpg photo.pdf --dpi=300x150
```

| Input | How it goes in |
|---|---|
| JPEG | The file's own DCT data, byte for byte (no re-compression). An APP2 ICC profile becomes the image's ICCBased colour space; a CMYK JPEG with an Adobe APP14 marker gets the inverted Decode array. 8-bit only. |
| PNG | Flate. Plain RGB/indexed PNGs keep their compressed rows; grey, alpha and 16-bit PNGs are decoded and re-Flated losslessly (16-bit stays 16-bit). Alpha and `tRNS` become an SMask. `iCCP` is kept. |
| TIFF | CCITT fax TIFFs keep their G3/G4 data; everything else is decoded and re-Flated losslessly (alpha as SMask, tag 34675 ICC kept). A multi-image TIFF wraps its first image, with a warning. |

The format is found from the file's bytes, not its extension (a `.jpg` that is really a PNG wraps as a PNG).

**DPI**: `--dpi=N` or `--dpi=XxY` wins; else the file's own — PNG `pHYs` (metre unit only; unit 0 is an aspect ratio), JPEG JFIF density (inch or cm), then EXIF X/YResolution (phone cameras write it only there), TIFF X/YResolution (inch when the unit tag is absent; unit 1 = no DPI); else 300 with the warning `the image states no resolution; wrapped at 300 dpi (give --dpi to choose)`. Resolutions are read from the file's bytes: ImageIO's TIFF reader invents 72 dpi for a file that states none, and Pillow invents 72 for a JPEG with EXIF but no resolution tags. Values within 0.0127 of a whole number snap to it (PNG stores pixels per metre).

**Orientation**: an EXIF Orientation (JPEG APP1, PNG `eXIf`, TIFF tag 274) is applied by the page's placement matrix, so the page shows the picture the way viewers do — a phone photo comes out upright — while the stored pixels (and the JPEG data) are untouched. Orientations 5–8 swap the page's width and height; `dpi` stays the stored pixels' DPI.

**Refused** (exit 2, nothing written): not a PNG/JPEG/TIFF; truncated or damaged files (a JPEG with no end-of-image marker after its scan); 12-bit JPEGs; 32-bit integer or floating-point samples (DEMs, HDR TIFFs), which have no PDF image form. Usage errors (no output, missing file, output = source, bad `--dpi`) exit 1. stderr stays empty.

Result: `format` (png\|jpeg\|tiff), `encoding` (dct\|flate\|ccitt), `colour` (gray\|rgb\|cmyk\|indexed), `bits`, `alpha`, `icc`, `width_px`, `height_px` (stored), `dpi[x, y]`, `dpi_source` (option\|file\|default), `orientation` (only when not 1), `page_size[w, h]` (points, as shown). The `--usage` sidecar records mode `wrap`, `pages_in` 1, `pages_out` 1.

Checker: `tools/wrap/make_wrap_fixtures.py OUTDIR` writes 47 real fixtures (Pillow + ImageMagick, never the jar); `tools/wrap/check_wrap.py JAR FIXDIR OUTDIR [--corrupt=dpi|pixels|icc|orient]` asserts page size, pixel count, bit depth, JPEG passthrough, SMask, ICC bytes, orientation and a Ghostscript render against the source, plus the damaged-file faults. Each `--corrupt` control must fail.

## 17.6d `--links-check` and `--relink` — an Illustrator master's placed files *(2.35.0)*

An Illustrator master saved as PDF carries two copies of every placed (linked) image: the page content, which Illustrator rendered and which is complete on its own, and the placed-file record in the Illustrator private data, which names the file by the absolute path on the designer's machine (`C:\Users\...\Links\x.pdf`). Illustrator reopens the master against that record, and the fileref image variables of §12 swap against it. Intake therefore needs two answers: does the package ship every linked file, and will the master reopen from the package on any machine.

```
java -jar java-epic-cli.jar --links-check master.pdf                 # Links/ beside the PDF
java -jar java-epic-cli.jar --links-check master.pdf --links=assets/
java -jar java-epic-cli.jar --relink master.pdf                      # in place
java -jar java-epic-cli.jar --relink master.pdf --output=relinked.pdf
java -jar java-epic-cli.jar --links-check package.zip                # 2.37.0: PDF at the zip root, Links/ inside
java -jar java-epic-cli.jar --relink package.zip --output=relinked.zip --package-root='C:\Jobs\v12'
```

**Package zip input** *(2.37.0)*: a `.zip` source is extracted to a scratch directory (removed afterwards), the PDF at the root is the one read (the largest, the `--derivatives` rule; `pdf` names the entry), and `Links/` beside it inside the zip is the default DIR (`--links=` may name another folder inside the package, or an absolute directory). `links_dir` and `resolved` are reported package-relative (`Links/x.pdf`). `--relink` on a zip delivers as asked: `--output=x.zip` is a copy of the zip with the PDF entry replaced and every other entry copied byte for byte (names, order and times kept), `--output=x.pdf` is the relinked PDF alone, no `--output` rewrites the zip in place through a temp file and an atomic rename. Inside a zip there is no absolute location to write into the records' path line: with `--package-root=DIR` it is written as DIR + the relative path (backslashes when DIR has a drive letter, as Illustrator writes on Windows), otherwise as the relative path itself and a warning says so; the `%AI28_BeginPlacedRelativePath` block is the one Illustrator falls back to either way.

**`--links-check`** lists every placed-file record of the first page that carries Illustrator data, and whether a file of that basename exists in DIR (default `Links/` next to the PDF). Nothing is written. Exit 0 when every file is found (also when the master has no placed files at all: `links_total` 0), exit 1 with the full list when any is missing.

**`--relink`** rewrites every record the way Illustrator's own *Package* command does: the path line and `%%IncludeFile` to the absolute path of the file found in DIR, and the `%AI28_BeginPlacedRelativePath` block to the path relative to the PDF's directory (`Links/x.pdf`), which is what Illustrator falls back to when the absolute path is not there. The page content, resources and every other object are untouched (renders are byte-identical). In place unless `--output`; in-place writes go through a temp file and an atomic rename. Exit 1 and nothing written when any file is missing from DIR. A record saved by an Illustrator older than 28 has no relative-path block: its absolute path is rewritten and a warning says the record cannot travel.

Files match by basename exactly, then case-insensitively with a warning (Illustrator on Windows and macOS accepts the case difference; Linux does not). A PDF with no Illustrator data at all exits 1 (`no Illustrator data in …`). stderr stays empty.

Result: `mode`, `links_dir`, `links_total`, `missing`, `rewritten` (relink), `links[]` with `name`, `path` (as the record names it), `relative` (the record's relative path; `""` when empty, null when the record has none), `bound_variable` (the variable whose docRef names this art, fileref or visibility, else null), `xml_uid`, `file_type`, `box_in{x,y,w,h}` (artboard inches, y-down from the trim's top-left, the same frame `--list-slots` reports), `found`, `resolved` (the file found in DIR), `rewritten` (relink: the relative path written).

Verified: a relinked master re-checks clean, renders byte-identical to its source, and `--derivatives` run from the relinked package produce the same PDFs (the only byte differences are the rewritten path strings in the private data). Not yet verified by reopening a relinked file in Illustrator; the record forms are the ones measured on native Illustrator 28–30 saves and the relative-path block is written as the 2.33.0 image swap writes it.

## 17.6e `--retype=SPEC.json` — retype a word that `--vectorize` traced *(2.36.0)*

A `--vectorize` output carries a logo's lettering as traced outlines. The studio user draws a rectangle around one word, types it, picks a face and size until it sits like the original, and accepts. This mode is the accept.

```
java -jar java-epic-cli.jar --retype=spec.json traced.pdf --output=retyped.pdf
java -jar java-epic-cli.jar --retype=spec.json traced.pdf --report-only
```

```json
{"region": {"x": 0.25, "y": 0.35, "w": 2.6, "h": 0.85},
 "text": "HELLO", "font": "DejaVuSans-Bold", "size": "fit", "tracking": 0,
 "align": "left", "ink": "Ink2",
 "mesh": {"bottom": [[0,0],[0.5,0.15],[1,0]], "top": [[0,1],[0.5,0.85],[1,1]]}}
```

| key | meaning |
|---|---|
| `region` | `{x,y,w,h}` in artboard inches, y-down from the TrimBox's top-left: the `box_in` frame every other mode reports. Required. |
| `text` | one line. Required. |
| `font` | a file path, or a name resolved in the `--fonts-dir` chain (`name`, `name.ttf`, `name.otf`). Required. |
| `size` | points, or `"fit"` (default): the largest size whose ink box, warped when a mesh is given, fits the region in both axes. |
| `tracking` | thousandths of an em between glyphs. Default 0. |
| `align` | `left`, `center` (default) or `right`: where the ink box sits in the region. Vertically it is centred. |
| `ink` | the name of a spot already in the file; `spot('Name',c,m,y,k)` to add one (0-100; an existing spot of that name is reused); or `cmyk(c,m,y,k)`. Required. An unknown bare name exits 1 and the message lists the file's spots. |
| `mesh` | optional one-row envelope, `{bottom:[[x,y]..], top:[[x,y]..]}` in the lab warp's dialect: anchors normalised to the straight text's ink box, `[0,0]` bottom-left, `[1,1]` top-right, two or more per row, the same count in both. |

**Removal** works per subpath, not per painting operator, because a vectorize output paints each ink as one compound path: a subpath whose page-space bounds lie wholly inside the region (0.02 pt slack) is dropped from its path and counted in `removed{ink}`; a subpath that only crosses the region stays and is counted in `partial{ink}`; a subpath inside the region that is a hole of a subpath staying behind stays too (dropping the hole would fill it in). A path that also sets a clip (`W`) is kept whole and warned. Removal runs across every ink, inside form XObjects too (a form drawn at two places is left as drawn, warned). Rasters, shadings and existing live text are never removed; they are counted (`images_untouched`, `text_runs_untouched`, `shadings_untouched`). The ink of a fill is its Separation colorant, the DeviceN colorants painted joined with `+`, or `cmyk`.

**The text** is laid out exactly as the lab compile lays a story (same kerning, same tracking, same ink-box law), so a word retyped here and the same word compiled from a master measure the same. Straight text goes onto the page as live `BT`/`Tf`/`TJ` text in a Type0 font embedded whole (never subset: Illustrator needs the face to edit the word); a warped word goes onto the page as filled outlines, and the warning says so.

**The Illustrator data** is re-authored from the page that remains, the same forge `--compile` uses: every kept fill as one compound path per spot, plus the text as a live story (inside an envelope mesh when `mesh` is given), so the file reopens in Illustrator as a native document with the word editable and the rest of the art intact. The forge can only carry spot fills: when the page keeps process-colour fills, tints, strokes, rasters, shadings or text, or the ink is `cmyk()`, the PDF is still written, the source's own Illustrator data is removed (left in place it would bring the removed glyphs back) and a warning says why; `ai_private` reports which happened. A MediaBox not at the origin skips the forge the same way.

**`--report-only`** writes nothing and adds `objects[]`: the text's outlines as one object in the `--extract-objects` dialect (`M`/`L`/`Z` absolute, `%.4f`, inches y-down from the trim's top-left, `fill_rule` nonzero, `bbox_in`), so a preview can be drawn without a render.

Result: `mode`, `region_in`, `text`, `font`, `font_file`, `size_pt` (the size used; the fitted one for `"fit"`), `tracking`, `align`, `ink`, `warped`, `live_text`, `ai_private`, `removed{ink:n}`, `partial{ink:n}`, `images_untouched`, `text_runs_untouched`, `shadings_untouched`, `text_box_in{x,y,w,h}` (the ink box placed), `warnings`, `objects[]` (report-only). Exit 1 on a spec fault, an unknown ink, a missing font, a region off the page or `--output` equal to the source (nothing written); 2 when the PDF cannot be read. First page only (a multi-page file is warned). stderr stays empty.

Measured on a vectorize of a 4 x 2 in raster (a blue word, a blue bar, a red disc): the five glyphs and the O's counterbore were the six subpaths removed from the blue plate, the bar and the disc untouched; the retyped file re-extracts as two compounds plus a `texts[]` run in the embedded face (`subset: false`), and `stories[]` finds the forged story, with `warp.style "mesh"` and the anchors when a mesh was given.

## 17.7 `--remove-color=NAME[;NAME]`

Deletes every object that paints the named spot colorant(s), however deep, and leaves everything
else byte-for-byte. Names match Separation colorants and DeviceN inks as `--list-spot-colors`
reports them; `--match=exact|ci|glob` (glob takes `*` and `?`, e.g. `"PANTONE *"`).

Removed: path fills/strokes in that Separation (a fill+stroke path loses only the matching
half), text runs by render mode, shadings and shading patterns, uncoloured tiling patterns,
image XObjects and inline images whose colourspace is that ink (the spot-swatched grayscale
TIF), stencil masks painted with it, and the same inside form XObjects to any depth. A DeviceN
image mixing the ink with others keeps the other channels and has that one zeroed. Kept and
warned: DeviceN paint mixing the ink at non-zero values, forms drawn from several sites under
different inherited paints, the colourspace resources themselves (the ink still lists, unused).

**The Illustrator payload is stripped by default** — Illustrator rebuilds the PDF paint from its
private copy on re-save, which would resurrect every removed object. `--keep-ai-payload` keeps it
(warned either way). Result: `colors[]`, `removed{fills, strokes, text_runs, images,
image_channels_zeroed, shadings, inline_images}`, `forms_walked`, `patterns_walked`,
`ai_payload_stripped`.

## 17.8 Examples

```
java -jar java-epic-cli.jar --edge="spot('White',0,0,0,0)" --edge-width=1mm --edge-join=round in.pdf out.pdf
java -jar java-epic-cli.jar --trap=0.25pt in.pdf out.pdf
java -jar java-epic-cli.jar --marks --marks-color="spot('Dieline',0,100,0,0)" --unit=mm --bleed=3 in.pdf out.pdf
java -jar java-epic-cli.jar --keyline --keyline-dash=4,2 --keyline-knockout in.pdf out.pdf
java -jar java-epic-cli.jar --expand-strokes --hairline=0.25pt out.pdf
java -jar java-epic-cli.jar --outline-text in.pdf out.pdf
java -jar java-epic-cli.jar --spot-to-process="PANTONE 485 C" in.pdf out.pdf
java -jar java-epic-cli.jar --overprint=on in.pdf out.pdf
java -jar java-epic-cli.jar --step=3x4 --gap=0.25 --sheet=13x19 in.pdf sheet.pdf
java -jar java-epic-cli.jar --remove-color="HA Adhesive" art.pdf art_no_adhesive.pdf
```
