print-check-cli 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +87 -44
  2. package/dist/index.js +22 -24
  3. package/package.json +26 -3
package/README.md CHANGED
@@ -1,23 +1,64 @@
1
1
  # print-check-cli
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/print-check-cli)](https://www.npmjs.com/package/print-check-cli)
3
4
  [![CI](https://github.com/ryancalacsan/print-check-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/ryancalacsan/print-check-cli/actions/workflows/ci.yml)
5
+ [![codecov](https://codecov.io/gh/ryancalacsan/print-check-cli/graph/badge.svg)](https://codecov.io/gh/ryancalacsan/print-check-cli)
4
6
 
5
7
  A Node.js + TypeScript CLI tool that validates print-ready PDF files. Runs eight checks and reports pass/warn/fail results in the terminal.
6
8
 
7
- ## Checks
9
+ **[View on npm](https://www.npmjs.com/package/print-check-cli)**
8
10
 
9
- | Check | What it validates |
10
- |---|---|
11
- | **Bleed & Trim** | TrimBox/BleedBox presence and minimum bleed dimensions |
12
- | **Fonts** | Font embedding status (embedded, subset, or missing) |
13
- | **Color Space** | CMYK compliance, RGB detection, spot color reporting |
14
- | **Resolution** | Raster image DPI against a configurable minimum |
15
- | **PDF/X Compliance** | PDF/X standard detection (OutputIntents, version, output condition) — info only |
16
- | **Total Ink Coverage** | Maximum ink density (C+M+Y+K %) against configurable limit |
17
- | **Transparency** | Detects unflattened transparency (groups, soft masks, blend modes) |
18
- | **Page Size** | Verifies consistent page dimensions and optional expected size match |
11
+ ![print-check-cli Preview](./preview.png)
19
12
 
20
- ## Usage
13
+ ## Demo
14
+
15
+ ![print-check demo](demo/demo.gif)
16
+
17
+ ## Features
18
+
19
+ - **8 prepress checks** — bleed/trim boxes, font embedding, color space (CMYK/RGB), image DPI, PDF/X compliance, total ink coverage, transparency, and page size consistency
20
+ - **Built-in print profiles** — presets for standard, magazine, newspaper, and large-format workflows with one `--profile` flag
21
+ - **Per-check severity overrides** — downgrade failures to warnings or skip checks entirely with `--severity fonts:warn,transparency:off`
22
+ - **Multi-file support** — validate entire directories with shell globbing (`print-check *.pdf`)
23
+ - **JSON output for CI** — structured JSON reports via `--format json` for pipeline integration
24
+ - **RC file configuration** — set project defaults in `.printcheckrc`, `.printcheckrc.json`, or `printcheck.config.js` with auto-discovery
25
+ - **Dual PDF engine** — combines mupdf (WASM-powered deep object traversal) with pdf-lib (page box reading) for thorough analysis
26
+ - **Colorized terminal output** — clear pass/warn/fail results with verbose per-page detail mode
27
+
28
+ | Check | What it validates |
29
+ | ---------------------- | ------------------------------------------------------------------------------- |
30
+ | **Bleed & Trim** | TrimBox/BleedBox presence and minimum bleed dimensions |
31
+ | **Fonts** | Font embedding status (embedded, subset, or missing) |
32
+ | **Color Space** | CMYK compliance, RGB detection, spot color reporting |
33
+ | **Resolution** | Raster image DPI against a configurable minimum |
34
+ | **PDF/X Compliance** | PDF/X standard detection (OutputIntents, version, output condition) — info only |
35
+ | **Total Ink Coverage** | Maximum ink density (C+M+Y+K %) against configurable limit |
36
+ | **Transparency** | Detects unflattened transparency (groups, soft masks, blend modes) |
37
+ | **Page Size** | Verifies consistent page dimensions and optional expected size match |
38
+
39
+ ## Tech Stack
40
+
41
+ | Package | Purpose |
42
+ | ------------------------------------------------------- | ----------------------------------------------------------- |
43
+ | [mupdf](https://www.npmjs.com/package/mupdf) (mupdf.js) | PDF engine — WASM-powered, deep PDF object traversal |
44
+ | [pdf-lib](https://www.npmjs.com/package/pdf-lib) | Supplemental — reading page boxes (TrimBox, BleedBox, etc.) |
45
+ | [commander](https://www.npmjs.com/package/commander) | CLI framework |
46
+ | [picocolors](https://www.npmjs.com/package/picocolors) | Terminal colors |
47
+ | [zod](https://www.npmjs.com/package/zod) | CLI option validation |
48
+ | [tsup](https://www.npmjs.com/package/tsup) | TypeScript build |
49
+ | [vitest](https://www.npmjs.com/package/vitest) | Testing |
50
+
51
+ ## Getting Started
52
+
53
+ ```bash
54
+ # Install globally
55
+ npm install -g print-check-cli
56
+
57
+ # Or run directly with npx
58
+ npx print-check-cli flyer.pdf
59
+ ```
60
+
61
+ ### Usage
21
62
 
22
63
  ```
23
64
  print-check <file.pdf ...> [options]
@@ -75,17 +116,12 @@ print-check *.pdf --format json
75
116
 
76
117
  Built-in profiles provide preset thresholds for common print scenarios. Explicit CLI flags override profile defaults.
77
118
 
78
- | Profile | minDpi | colorSpace | bleedMm | maxTac | Use case |
79
- |---------|--------|------------|---------|--------|----------|
80
- | `standard` | 300 | cmyk | 3 | 300 | General commercial print (default) |
81
- | `magazine` | 300 | cmyk | 5 | 300 | Magazine / perfect-bound |
82
- | `newspaper` | 150 | any | 0 | 240 | Newsprint / low-fidelity |
83
- | `large-format` | 150 | cmyk | 5 | 300 | Banners, posters, signage |
84
-
85
- ### Exit codes
86
-
87
- - `0` — all checks passed (or warned)
88
- - `1` — one or more checks failed
119
+ | Profile | minDpi | colorSpace | bleedMm | maxTac | Use case |
120
+ | -------------- | ------ | ---------- | ------- | ------ | ---------------------------------- |
121
+ | `standard` | 300 | cmyk | 3 | 300 | General commercial print (default) |
122
+ | `magazine` | 300 | cmyk | 5 | 300 | Magazine / perfect-bound |
123
+ | `newspaper` | 150 | any | 0 | 240 | Newsprint / low-fidelity |
124
+ | `large-format` | 150 | cmyk | 5 | 300 | Banners, posters, signage |
89
125
 
90
126
  ### Severity Overrides
91
127
 
@@ -102,19 +138,25 @@ print-check flyer.pdf --severity transparency:off
102
138
  print-check flyer.pdf --severity fonts:warn,transparency:off
103
139
  ```
104
140
 
105
- | Level | Behavior |
106
- |-------|----------|
107
- | `fail` | Default — no change to check result |
141
+ | Level | Behavior |
142
+ | ------ | ---------------------------------------------- |
143
+ | `fail` | Default — no change to check result |
108
144
  | `warn` | Downgrade any `fail` result to `warn` (exit 0) |
109
- | `off` | Skip the check entirely |
145
+ | `off` | Skip the check entirely |
110
146
 
111
147
  Available check names: `bleed`, `fonts`, `colorspace`, `resolution`, `pdfx`, `tac`, `transparency`, `pagesize`.
112
148
 
113
- ## Configuration
149
+ ### Exit Codes
150
+
151
+ - `0` — all checks passed (or warned)
152
+ - `1` — one or more checks failed
153
+
154
+ ### Configuration
114
155
 
115
156
  Create a config file to set default options for your project:
116
157
 
117
- ### `.printcheckrc` / `.printcheckrc.json`
158
+ #### `.printcheckrc` / `.printcheckrc.json`
159
+
118
160
  ```json
119
161
  {
120
162
  "minDpi": 300,
@@ -130,7 +172,8 @@ Create a config file to set default options for your project:
130
172
  }
131
173
  ```
132
174
 
133
- ### `printcheck.config.js`
175
+ #### `printcheck.config.js`
176
+
134
177
  ```js
135
178
  export default {
136
179
  minDpi: 150,
@@ -143,18 +186,6 @@ export default {
143
186
  Config files are auto-discovered from the current directory upward.
144
187
  CLI flags always override config file values.
145
188
 
146
- ## Tech Stack
147
-
148
- | Package | Purpose |
149
- |---|---|
150
- | [mupdf](https://www.npmjs.com/package/mupdf) (mupdf.js) | PDF engine — WASM-powered, deep PDF object traversal |
151
- | [pdf-lib](https://www.npmjs.com/package/pdf-lib) | Supplemental — reading page boxes (TrimBox, BleedBox, etc.) |
152
- | [commander](https://www.npmjs.com/package/commander) | CLI framework |
153
- | [picocolors](https://www.npmjs.com/package/picocolors) | Terminal colors |
154
- | [zod](https://www.npmjs.com/package/zod) | CLI option validation |
155
- | [tsup](https://www.npmjs.com/package/tsup) | TypeScript build |
156
- | [vitest](https://www.npmjs.com/package/vitest) | Testing |
157
-
158
189
  ## Project Structure
159
190
 
160
191
  ```
@@ -182,13 +213,25 @@ src/
182
213
  ## Development
183
214
 
184
215
  ```bash
185
- npm install # Install dependencies
216
+ npm install # Install dependencies (also sets up pre-commit hooks)
186
217
  npm run dev -- <file> # Run via tsx (no build needed)
187
218
  npm run build # Build to dist/
188
219
  npm test # Run vitest
220
+ npm run test:coverage # Run with coverage report
221
+ npm run lint # ESLint
222
+ npm run format:check # Prettier check
223
+ ```
224
+
225
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for full development guidelines.
226
+
227
+ ## Deployment
228
+
229
+ Published to npm via GitHub Actions with [OIDC trusted publishing](https://docs.npmjs.com/generating-provenance-statements). Every release includes a verified provenance attestation.
230
+
231
+ ```bash
232
+ npm install -g print-check-cli
189
233
  ```
190
234
 
191
235
  ## Known Limitations (MVP)
192
236
 
193
237
  - **mupdf PDFObject nulls** — mupdf.js returns PDFObject wrappers with `.isNull() === true` rather than JavaScript `null`. All mupdf access goes through `src/engine/pdf-utils.ts` safe wrappers to handle this.
194
-
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { Command } from "commander";
5
5
  import { z as z2 } from "zod";
6
6
  import * as fs3 from "fs";
7
7
  import * as path2 from "path";
8
+ import { createRequire } from "module";
8
9
 
9
10
  // src/checks/bleed-trim.ts
10
11
  var PT_TO_MM = 25.4 / 72;
@@ -49,11 +50,9 @@ var checkBleedTrim = async (engines, options) => {
49
50
  if (minBleed < requiredMm) {
50
51
  const sides = [];
51
52
  if (bleedLeft < requiredMm) sides.push(`left: ${bleedLeft.toFixed(1)}mm`);
52
- if (bleedRight < requiredMm)
53
- sides.push(`right: ${bleedRight.toFixed(1)}mm`);
53
+ if (bleedRight < requiredMm) sides.push(`right: ${bleedRight.toFixed(1)}mm`);
54
54
  if (bleedTop < requiredMm) sides.push(`top: ${bleedTop.toFixed(1)}mm`);
55
- if (bleedBottom < requiredMm)
56
- sides.push(`bottom: ${bleedBottom.toFixed(1)}mm`);
55
+ if (bleedBottom < requiredMm) sides.push(`bottom: ${bleedBottom.toFixed(1)}mm`);
57
56
  details.push({
58
57
  page: pageNum,
59
58
  message: `Insufficient bleed (need ${requiredMm}mm): ${sides.join(", ")}`,
@@ -109,7 +108,7 @@ function collectFonts(fontDict, pageNum, seen) {
109
108
  safeForEach(fontDict, (value, key) => {
110
109
  const font = safeResolve(value);
111
110
  if (!font) return;
112
- const baseFontName = safeName(safeGet(font, "BaseFont")) ?? key;
111
+ const baseFontName = safeName(safeGet(font, "BaseFont")) ?? String(key);
113
112
  if (seen.has(baseFontName)) return;
114
113
  seen.add(baseFontName);
115
114
  const subtype = safeName(safeGet(font, "Subtype"));
@@ -703,10 +702,7 @@ import * as mupdf5 from "mupdf";
703
702
  import { PDFDocument as PDFDocument2 } from "pdf-lib";
704
703
  async function loadPdf(filePath) {
705
704
  const buffer = fs.readFileSync(filePath);
706
- const mupdfDoc = mupdf5.PDFDocument.openDocument(
707
- buffer,
708
- "application/pdf"
709
- );
705
+ const mupdfDoc = mupdf5.PDFDocument.openDocument(buffer, "application/pdf");
710
706
  const pdfLibDoc = await PDFDocument2.load(buffer, {
711
707
  ignoreEncryption: true
712
708
  });
@@ -768,12 +764,7 @@ function buildJsonReport(fileName, results) {
768
764
  }
769
765
 
770
766
  // src/profiles.ts
771
- var PROFILE_NAMES = [
772
- "standard",
773
- "magazine",
774
- "newspaper",
775
- "large-format"
776
- ];
767
+ var PROFILE_NAMES = ["standard", "magazine", "newspaper", "large-format"];
777
768
  var PROFILES = {
778
769
  standard: { minDpi: 300, colorSpace: "cmyk", bleedMm: 3, maxTac: 300, pageSize: void 0 },
779
770
  magazine: { minDpi: 300, colorSpace: "cmyk", bleedMm: 5, maxTac: 300, pageSize: void 0 },
@@ -798,11 +789,7 @@ var ConfigSchema = z.object({
798
789
  profile: z.enum(PROFILE_NAMES).optional(),
799
790
  severity: z.record(z.string(), z.enum(["fail", "warn", "off"])).optional()
800
791
  });
801
- var CONFIG_FILES = [
802
- ".printcheckrc",
803
- ".printcheckrc.json",
804
- "printcheck.config.js"
805
- ];
792
+ var CONFIG_FILES = [".printcheckrc", ".printcheckrc.json", "printcheck.config.js"];
806
793
  function findConfigFile(startDir) {
807
794
  let dir = path.resolve(startDir);
808
795
  while (true) {
@@ -839,12 +826,18 @@ async function loadConfig() {
839
826
  }
840
827
 
841
828
  // src/index.ts
829
+ var require2 = createRequire(import.meta.url);
830
+ var { version } = require2("../package.json");
831
+ var SEVERITY_LEVELS = ["fail", "warn", "off"];
832
+ function isSeverityOverride(level) {
833
+ return SEVERITY_LEVELS.includes(level);
834
+ }
842
835
  function parseSeverityString(val) {
843
836
  if (!val.trim()) return {};
844
837
  const result = {};
845
838
  for (const pair of val.split(",")) {
846
839
  const [check, level] = pair.split(":").map((s) => s.trim());
847
- if (check && level) result[check] = level;
840
+ if (check && level && isSeverityOverride(level)) result[check] = level;
848
841
  }
849
842
  return result;
850
843
  }
@@ -877,14 +870,19 @@ var OptionsSchema = z2.object({
877
870
  bleed: z2.coerce.number().nonnegative().optional(),
878
871
  maxTac: z2.coerce.number().positive().optional(),
879
872
  pageSize: z2.string().optional(),
880
- checks: z2.string().default("all").transform((val) => val === "all" ? Object.keys(ALL_CHECKS) : val.split(",").map((s) => s.trim())),
873
+ checks: z2.string().default("all").transform(
874
+ (val) => val === "all" ? Object.keys(ALL_CHECKS) : val.split(",").map((s) => s.trim())
875
+ ),
881
876
  verbose: z2.boolean().default(false),
882
877
  format: z2.enum(["text", "json"]).default("text"),
883
878
  profile: z2.enum(PROFILE_NAMES).optional(),
884
- severity: z2.union([z2.string().transform(parseSeverityString), z2.record(z2.string(), z2.enum(["fail", "warn", "off"]))]).default({})
879
+ severity: z2.union([
880
+ z2.string().transform(parseSeverityString),
881
+ z2.record(z2.string(), z2.enum(["fail", "warn", "off"]))
882
+ ]).default({})
885
883
  });
886
884
  var program = new Command();
887
- program.name("print-check").description("Validate print-ready PDF files").version("1.0.0").argument("<files...>", "PDF file(s) to check").option("--min-dpi <number>", "Minimum acceptable DPI").option("--color-space <mode>", "Expected color space: cmyk | any").option("--bleed <mm>", "Required bleed in mm").option("--max-tac <percent>", "Maximum total ink coverage %").option("--page-size <WxH>", "Expected page size in mm (e.g. 210x297)").option("--checks <list>", "Comma-separated checks to run", "all").option("--verbose", "Show detailed per-page results", false).option("--format <type>", "Output format: text | json", "text").option("--profile <name>", "Print profile: standard | magazine | newspaper | large-format").option("--severity <overrides>", "Per-check severity: check:level,... (fail|warn|off)").action(async (files, rawOpts) => {
885
+ program.name("print-check").description("Validate print-ready PDF files").version(version).argument("<files...>", "PDF file(s) to check").option("--min-dpi <number>", "Minimum acceptable DPI").option("--color-space <mode>", "Expected color space: cmyk | any").option("--bleed <mm>", "Required bleed in mm").option("--max-tac <percent>", "Maximum total ink coverage %").option("--page-size <WxH>", "Expected page size in mm (e.g. 210x297)").option("--checks <list>", "Comma-separated checks to run", "all").option("--verbose", "Show detailed per-page results", false).option("--format <type>", "Output format: text | json", "text").option("--profile <name>", "Print profile: standard | magazine | newspaper | large-format").option("--severity <overrides>", "Per-check severity: check:level,... (fail|warn|off)").action(async (files, rawOpts) => {
888
886
  const config = await loadConfig();
889
887
  const stripped = {};
890
888
  for (const [key, value] of Object.entries(rawOpts)) {
package/package.json CHANGED
@@ -1,14 +1,20 @@
1
1
  {
2
2
  "name": "print-check-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "print-check": "./dist/index.js"
7
7
  },
8
8
  "scripts": {
9
9
  "build": "tsup",
10
+ "typecheck": "tsc --noEmit",
10
11
  "dev": "tsx src/index.ts",
11
- "test": "vitest run"
12
+ "test": "vitest run",
13
+ "test:coverage": "vitest run --coverage",
14
+ "lint": "eslint src/ tests/",
15
+ "format": "prettier --write .",
16
+ "format:check": "prettier --check .",
17
+ "prepare": "husky"
12
18
  },
13
19
  "keywords": [
14
20
  "pdf",
@@ -42,11 +48,28 @@
42
48
  "picocolors": "^1.1.1",
43
49
  "zod": "^4.3.6"
44
50
  },
51
+ "lint-staged": {
52
+ "*.{ts,js}": [
53
+ "eslint --fix",
54
+ "prettier --write"
55
+ ],
56
+ "*.{json,md,yml,yaml}": [
57
+ "prettier --write"
58
+ ]
59
+ },
45
60
  "devDependencies": {
46
- "@types/node": "^25.1.0",
61
+ "@eslint/js": "^10.0.1",
62
+ "@types/node": "^26.0.0",
63
+ "@vitest/coverage-v8": "^4.0.18",
64
+ "eslint": "^10.5.0",
65
+ "eslint-config-prettier": "^10.1.8",
66
+ "husky": "^9.1.7",
67
+ "lint-staged": "^16.2.7",
68
+ "prettier": "^3.8.1",
47
69
  "tsup": "^8.5.1",
48
70
  "tsx": "^4.21.0",
49
71
  "typescript": "^5.9.3",
72
+ "typescript-eslint": "^8.54.0",
50
73
  "vitest": "^4.0.18"
51
74
  }
52
75
  }