gutterpress 0.11.14 → 0.11.15-alpha.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 (27) hide show
  1. package/dist/api/index.js +2 -2
  2. package/dist/{build-czwbmbsb.js → build-4522yqmm.js} +4 -4
  3. package/dist/{cli-vgjtz2e6.js → cli-7pw53gnm.js} +270 -8
  4. package/dist/{cli-7z0j8sqj.js → cli-b496rxy0.js} +1 -1
  5. package/dist/{cli-tjhn2mdn.js → cli-mc3waxaw.js} +9 -11
  6. package/dist/{cli-1723rkd1.js → cli-vdpnctkq.js} +1 -1
  7. package/dist/cli.js +12 -12
  8. package/dist/{doctor-ej9e88dc.js → doctor-fgvwe9j2.js} +2 -2
  9. package/dist/engine/compiler/build.d.ts +2 -0
  10. package/dist/engine/compiler/postprocess.d.ts +6 -0
  11. package/dist/{engine-132czsq9.js → engine-h8zdtxhp.js} +1 -1
  12. package/dist/{engine-bgd4aa99.js → engine-s18wy689.js} +2 -2
  13. package/dist/{ext-47cs7bp8.js → ext-v44phkxz.js} +4 -4
  14. package/dist/{index-nm81wpfb.js → index-j77039xr.js} +269 -7
  15. package/dist/{index-mw8b25yx.js → index-w6tqhafk.js} +1 -1
  16. package/dist/{index-b6p1fh61.js → index-ypxhxvwv.js} +9 -11
  17. package/dist/index.js +3 -3
  18. package/dist/lib/black-text-gray.d.ts +40 -0
  19. package/dist/lib/engine.d.ts +1 -0
  20. package/dist/{manifest.schema-61bsspsb.json → manifest.schema-40sfq49z.json} +22 -0
  21. package/dist/{new-y5kn67a5.js → new-7npm260n.js} +4 -4
  22. package/dist/{preflight-aj2cps9w.js → preflight-y0jhsz8p.js} +4 -4
  23. package/dist/{preview-mk98eva0.js → preview-xkp190pd.js} +4 -4
  24. package/dist/{publish-q7zpx8yq.js → publish-czx47htf.js} +4 -4
  25. package/dist/schema/manifest.types.d.ts +10 -0
  26. package/dist/{validate-scqwwwx5.js → validate-x89z3zrq.js} +4 -4
  27. package/package.json +1 -1
package/dist/api/index.js CHANGED
@@ -129,10 +129,10 @@ import {
129
129
  testRemoteAccess,
130
130
  updateExtensions,
131
131
  validateProjectExtensions
132
- } from "../index-nm81wpfb.js";
132
+ } from "../index-j77039xr.js";
133
133
  import {
134
134
  BuildError
135
- } from "../index-mw8b25yx.js";
135
+ } from "../index-w6tqhafk.js";
136
136
  import {
137
137
  DEFAULT_PRINT_OPTS,
138
138
  assertMilestone,
@@ -1,18 +1,18 @@
1
1
  import {
2
2
  runBuild,
3
3
  splitOutPath
4
- } from "./cli-vgjtz2e6.js";
4
+ } from "./cli-7pw53gnm.js";
5
5
  import {
6
6
  log
7
- } from "./cli-7z0j8sqj.js";
7
+ } from "./cli-b496rxy0.js";
8
8
  import {
9
9
  UsageError,
10
10
  parseFormat,
11
11
  parsePdfxFlavor,
12
12
  rejectExtraPositionals,
13
13
  rejectUnknownFlags
14
- } from "./cli-1723rkd1.js";
15
- import"./cli-tjhn2mdn.js";
14
+ } from "./cli-vdpnctkq.js";
15
+ import"./cli-mc3waxaw.js";
16
16
  import {
17
17
  BuildError
18
18
  } from "./cli-46ycxe6r.js";
@@ -13,17 +13,17 @@ import {
13
13
  stampCreator,
14
14
  stripAnnotations,
15
15
  warn
16
- } from "./cli-7z0j8sqj.js";
16
+ } from "./cli-b496rxy0.js";
17
17
  import {
18
18
  DEBOUNCE,
19
19
  UsageError,
20
20
  resolvePort
21
- } from "./cli-1723rkd1.js";
21
+ } from "./cli-vdpnctkq.js";
22
22
  import {
23
23
  MARGIN_BOX_IGNORED_PROPERTIES,
24
24
  getAssetPath,
25
25
  getAssetsDir
26
- } from "./cli-tjhn2mdn.js";
26
+ } from "./cli-mc3waxaw.js";
27
27
  import {
28
28
  BuildError
29
29
  } from "./cli-46ycxe6r.js";
@@ -105,11 +105,12 @@ var DTRPG_PRESET = {
105
105
  source: {
106
106
  files: null
107
107
  },
108
- print: { signature: 1 },
108
+ print: { signature: 4, reserveLastPage: true },
109
109
  pdfx: {
110
110
  flavor: "x1a",
111
111
  icc: "profiles/CGATS21_CRPC1.icc",
112
- stripAnnotations: true
112
+ stripAnnotations: true,
113
+ blackText: "k-only"
113
114
  },
114
115
  page: {
115
116
  width: 621,
@@ -179,11 +180,12 @@ var BOOK_PRESET = {
179
180
  source: {
180
181
  files: null
181
182
  },
182
- print: { signature: 1 },
183
+ print: { signature: 1, reserveLastPage: false },
183
184
  pdfx: {
184
185
  flavor: "x1a",
185
186
  icc: "profiles/CGATS21_CRPC1.icc",
186
- stripAnnotations: true
187
+ stripAnnotations: true,
188
+ blackText: "icc"
187
189
  },
188
190
  page: {
189
191
  width: 432,
@@ -7436,6 +7438,261 @@ async function writeBuildFingerprint(input) {
7436
7438
  return outPath;
7437
7439
  }
7438
7440
 
7441
+ // src/lib/black-text-gray.ts
7442
+ var MAX_CHANNEL = 0.12;
7443
+ var MAX_SPREAD = 0.04;
7444
+ function isNearBlack([r, g, b]) {
7445
+ const max = Math.max(r, g, b);
7446
+ return max <= MAX_CHANNEL && max - Math.min(r, g, b) <= MAX_SPREAD;
7447
+ }
7448
+ var WS = new Set([0, 9, 10, 12, 13, 32]);
7449
+ var DELIM = new Set("()<>[]{}/%".split("").map((c) => c.charCodeAt(0)));
7450
+ function* tokenize(s) {
7451
+ let i = 0;
7452
+ const n = s.length;
7453
+ while (i < n) {
7454
+ const c = s.charCodeAt(i);
7455
+ if (WS.has(c)) {
7456
+ i++;
7457
+ continue;
7458
+ }
7459
+ const start = i;
7460
+ if (c === 37) {
7461
+ while (i < n && s[i] !== `
7462
+ ` && s[i] !== "\r")
7463
+ i++;
7464
+ continue;
7465
+ }
7466
+ if (c === 40) {
7467
+ let depth = 0;
7468
+ while (i < n) {
7469
+ const d = s[i++];
7470
+ if (d === "\\")
7471
+ i++;
7472
+ else if (d === "(")
7473
+ depth++;
7474
+ else if (d === ")" && --depth === 0)
7475
+ break;
7476
+ }
7477
+ yield { text: s.slice(start, i), start, end: i, op: false };
7478
+ continue;
7479
+ }
7480
+ if (c === 60 && s[i + 1] === "<") {
7481
+ i += 2;
7482
+ yield { text: "<<", start, end: i, op: false };
7483
+ continue;
7484
+ }
7485
+ if (c === 62 && s[i + 1] === ">") {
7486
+ i += 2;
7487
+ yield { text: ">>", start, end: i, op: false };
7488
+ continue;
7489
+ }
7490
+ if (c === 60) {
7491
+ while (i < n && s[i] !== ">")
7492
+ i++;
7493
+ i++;
7494
+ yield { text: s.slice(start, i), start, end: i, op: false };
7495
+ continue;
7496
+ }
7497
+ if (c === 91 || c === 93 || c === 62 || c === 123 || c === 125 || c === 41) {
7498
+ i++;
7499
+ yield { text: s[start], start, end: i, op: false };
7500
+ continue;
7501
+ }
7502
+ i++;
7503
+ while (i < n && !WS.has(s.charCodeAt(i)) && !DELIM.has(s.charCodeAt(i)))
7504
+ i++;
7505
+ const text = s.slice(start, i);
7506
+ const isOperand = c === 47 || /^[+-]?(\d+\.?\d*|\.\d+)$/.test(text);
7507
+ yield { text, start, end: i, op: !isOperand };
7508
+ if (text === "ID") {
7509
+ const m = /\sEI(?=\s|$)/g;
7510
+ m.lastIndex = i;
7511
+ const hit = m.exec(s);
7512
+ i = hit ? hit.index + hit[0].length : n;
7513
+ }
7514
+ }
7515
+ }
7516
+ function rewriteBlackText(src) {
7517
+ let fill = null;
7518
+ let stroke = null;
7519
+ const stack = [];
7520
+ let inText = false;
7521
+ let fillDirty = false;
7522
+ let strokeDirty = false;
7523
+ const edits = [];
7524
+ let operands = [];
7525
+ const rgbOf = () => {
7526
+ if (operands.length !== 3)
7527
+ return null;
7528
+ const v = operands.map((t) => Number(t.text));
7529
+ return v.some((x) => Number.isNaN(x)) ? null : { rgb: v, raw: operands.map((t) => t.text).join(" ") };
7530
+ };
7531
+ for (const t of tokenize(src)) {
7532
+ if (!t.op) {
7533
+ operands.push(t);
7534
+ continue;
7535
+ }
7536
+ const op = t.text;
7537
+ switch (op) {
7538
+ case "q":
7539
+ stack.push([fill, stroke]);
7540
+ break;
7541
+ case "Q": {
7542
+ const p = stack.pop();
7543
+ if (p)
7544
+ [fill, stroke] = p;
7545
+ break;
7546
+ }
7547
+ case "BT":
7548
+ inText = true;
7549
+ fillDirty = strokeDirty = false;
7550
+ {
7551
+ let ins = "";
7552
+ if (fill && isNearBlack(fill.rgb)) {
7553
+ ins += " 0 g";
7554
+ fillDirty = true;
7555
+ }
7556
+ if (stroke && isNearBlack(stroke.rgb)) {
7557
+ ins += " 0 G";
7558
+ strokeDirty = true;
7559
+ }
7560
+ if (ins)
7561
+ edits.push({ start: t.end, end: t.end, text: ins });
7562
+ }
7563
+ break;
7564
+ case "ET":
7565
+ if (inText) {
7566
+ let ins = "";
7567
+ if (fillDirty && fill)
7568
+ ins += `${fill.raw} rg `;
7569
+ if (strokeDirty && stroke)
7570
+ ins += `${stroke.raw} RG `;
7571
+ if (ins)
7572
+ edits.push({ start: t.start, end: t.start, text: ins });
7573
+ }
7574
+ inText = false;
7575
+ break;
7576
+ case "rg":
7577
+ case "RG": {
7578
+ const c = rgbOf();
7579
+ const isFill = op === "rg";
7580
+ if (isFill)
7581
+ fill = c;
7582
+ else
7583
+ stroke = c;
7584
+ if (inText && c && isNearBlack(c.rgb)) {
7585
+ edits.push({ start: operands[0].start, end: t.end, text: isFill ? "0 g" : "0 G" });
7586
+ if (isFill)
7587
+ fillDirty = true;
7588
+ else
7589
+ strokeDirty = true;
7590
+ } else if (inText) {
7591
+ if (isFill)
7592
+ fillDirty = false;
7593
+ else
7594
+ strokeDirty = false;
7595
+ }
7596
+ break;
7597
+ }
7598
+ case "g":
7599
+ case "k":
7600
+ case "sc":
7601
+ case "scn":
7602
+ case "cs":
7603
+ fill = null;
7604
+ if (inText)
7605
+ fillDirty = false;
7606
+ break;
7607
+ case "G":
7608
+ case "K":
7609
+ case "SC":
7610
+ case "SCN":
7611
+ case "CS":
7612
+ stroke = null;
7613
+ if (inText)
7614
+ strokeDirty = false;
7615
+ break;
7616
+ }
7617
+ operands = [];
7618
+ }
7619
+ return applyEdits(src, edits);
7620
+ }
7621
+ function applyEdits(s, edits) {
7622
+ if (!edits.length)
7623
+ return s;
7624
+ let out = "";
7625
+ let pos = 0;
7626
+ for (const e of edits) {
7627
+ out += s.slice(pos, e.start) + e.text;
7628
+ pos = e.end;
7629
+ }
7630
+ return out + s.slice(pos);
7631
+ }
7632
+ async function blackTextToGray(pdf) {
7633
+ const { PDFDocument, PDFName, PDFRawStream, PDFArray, PDFRef, decodePDFRawStream } = await import("pdf-lib");
7634
+ const doc = await PDFDocument.load(pdf, { updateMetadata: false });
7635
+ const ctx = doc.context;
7636
+ const SKIP = ["Filter", "DecodeParms", "Length"].map((k) => PDFName.of(k));
7637
+ let changed = false;
7638
+ const decode = (s) => {
7639
+ try {
7640
+ return Buffer.from(decodePDFRawStream(s).decode()).toString("latin1");
7641
+ } catch {
7642
+ return null;
7643
+ }
7644
+ };
7645
+ const replace = (ref, old, text) => {
7646
+ const entries = {};
7647
+ for (const [k, v] of old.dict.entries())
7648
+ if (!SKIP.includes(k))
7649
+ entries[k.decodeText()] = v;
7650
+ ctx.assign(ref, ctx.flateStream(Buffer.from(text, "latin1"), entries));
7651
+ changed = true;
7652
+ };
7653
+ for (const [ref, obj] of ctx.enumerateIndirectObjects()) {
7654
+ if (!(obj instanceof PDFRawStream))
7655
+ continue;
7656
+ if (obj.dict.get(PDFName.of("Subtype")) !== PDFName.of("Form"))
7657
+ continue;
7658
+ const text = decode(obj);
7659
+ if (text === null)
7660
+ continue;
7661
+ const next = rewriteBlackText(text);
7662
+ if (next !== text)
7663
+ replace(ref, obj, next);
7664
+ }
7665
+ for (const page of doc.getPages()) {
7666
+ const contents = page.node.Contents();
7667
+ const refs = [];
7668
+ if (contents instanceof PDFArray) {
7669
+ for (let i = 0;i < contents.size(); i++) {
7670
+ const r = contents.get(i);
7671
+ if (r instanceof PDFRef)
7672
+ refs.push(r);
7673
+ }
7674
+ } else if (contents instanceof PDFRawStream) {
7675
+ const r = page.node.get(PDFName.of("Contents"));
7676
+ if (r instanceof PDFRef)
7677
+ refs.push(r);
7678
+ }
7679
+ const streams = refs.map((r) => ctx.lookup(r));
7680
+ if (!refs.length || !streams.every((s) => s instanceof PDFRawStream))
7681
+ continue;
7682
+ const parts = streams.map((s) => decode(s));
7683
+ if (parts.some((p) => p === null))
7684
+ continue;
7685
+ const joined = parts.join(`
7686
+ `);
7687
+ const next = rewriteBlackText(joined);
7688
+ if (next === joined)
7689
+ continue;
7690
+ replace(refs[0], streams[0], next);
7691
+ page.node.set(PDFName.of("Contents"), refs[0]);
7692
+ }
7693
+ return changed ? doc.save({ updateFieldAppearances: false }) : pdf;
7694
+ }
7695
+
7439
7696
  // src/lib/build-preflight.ts
7440
7697
  var INSTALL_HINTS2 = {
7441
7698
  gs: INSTALL_HINTS.gs.body,
@@ -7728,11 +7985,12 @@ class PdfOutput {
7728
7985
  const rawPdf = pdfxMode ? path8.join(stage, "raw.pdf") : path8.resolve(pdfFile);
7729
7986
  await fsp2.mkdir(path8.dirname(path8.resolve(pdfFile)), { recursive: true });
7730
7987
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
7731
- const { buildNativePdf } = await import("./engine-132czsq9.js");
7988
+ const { buildNativePdf } = await import("./engine-h8zdtxhp.js");
7732
7989
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
7733
7990
  title: config.title,
7734
7991
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
7735
7992
  signature: config.print.signature,
7993
+ reserveLastPage: config.print.reserveLastPage,
7736
7994
  allowShrink: opts.allowShrink
7737
7995
  }, browser);
7738
7996
  for (const d of engineDiagnostics)
@@ -7758,6 +8016,10 @@ class PdfOutput {
7758
8016
  log.info("Stripping annotations for PDF/X compliance");
7759
8017
  await stripAnnotations(rawPdf, stage);
7760
8018
  }
8019
+ if (config.pdfx.blackText === "k-only") {
8020
+ log.info("Rewriting near-black text to K-only gray");
8021
+ await fsp2.writeFile(rawPdf, await blackTextToGray(await fsp2.readFile(rawPdf)));
8022
+ }
7761
8023
  log.info(`Converting to CMYK PDF/X (${pdfxMode})`);
7762
8024
  await convertToPdfxCmyk(rawPdf, path8.resolve(pdfFile), {
7763
8025
  iccPath: effectiveIccPath,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  package_default
3
- } from "./cli-1723rkd1.js";
3
+ } from "./cli-vdpnctkq.js";
4
4
  import {
5
5
  execCapture,
6
6
  findTool,
@@ -1106,7 +1106,7 @@ import { fileURLToPath } from "node:url";
1106
1106
  var favicon_default = "./favicon-wkbm9cjn.ico";
1107
1107
 
1108
1108
  // src/assets/manifest.schema.json
1109
- var manifest_schema_default = "./manifest.schema-61bsspsb.json";
1109
+ var manifest_schema_default = "./manifest.schema-40sfq49z.json";
1110
1110
 
1111
1111
  // src/assets/preview/scripts/preview-interface.js
1112
1112
  var preview_interface_default = "./preview-interface-twd6nsf5.js";
@@ -1450,15 +1450,12 @@ async function postprocess(input, opts) {
1450
1450
  g.media.width - inset,
1451
1451
  g.media.height - inset
1452
1452
  ];
1453
- let padded = 0;
1454
- if (opts.signature && opts.signature > 1) {
1455
- const remainder = doc.getPageCount() % opts.signature;
1456
- if (remainder !== 0) {
1457
- padded = opts.signature - remainder;
1458
- for (let i = 0;i < padded; i++)
1459
- doc.addPage([g.media.width, g.media.height]);
1460
- }
1461
- }
1453
+ const signature = opts.signature && opts.signature > 1 ? opts.signature : 1;
1454
+ let padded = (signature - doc.getPageCount() % signature) % signature;
1455
+ if (padded === 0 && opts.reserveLastPage)
1456
+ padded = signature;
1457
+ for (let i = 0;i < padded; i++)
1458
+ doc.addPage([g.media.width, g.media.height]);
1462
1459
  const wantMarks = opts.marks || g.marks.includes("crop") || g.marks.includes("cross");
1463
1460
  for (const page of doc.getPages()) {
1464
1461
  const node = page.node;
@@ -2233,11 +2230,12 @@ ${mapCss}`;
2233
2230
  const post = await postprocess(bytes, {
2234
2231
  geometry: tier2.geometry,
2235
2232
  signature: opts.signature,
2233
+ reserveLastPage: opts.reserveLastPage,
2236
2234
  marks: opts.marks,
2237
2235
  title: opts.title,
2238
2236
  author: opts.author
2239
2237
  });
2240
- log(`postprocess: ${post.pageCount} pages` + (post.padded ? ` (+${post.padded} signature pad)` : ""));
2238
+ log(`postprocess: ${post.pageCount} pages` + (post.padded ? ` (+${post.padded} blank${opts.reserveLastPage ? "; last page reserved blank" : ""})` : ""));
2241
2239
  return {
2242
2240
  bytes: post.bytes,
2243
2241
  tier,
@@ -4,7 +4,7 @@ import {
4
4
  // package.json
5
5
  var package_default = {
6
6
  name: "gutterpress",
7
- version: "0.11.14",
7
+ version: "0.11.15-alpha.2",
8
8
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
9
9
  author: "itlackey",
10
10
  license: "MPL-2.0",
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  UsageError,
7
7
  package_default,
8
8
  rejectUnknownFlags
9
- } from "./cli-1723rkd1.js";
9
+ } from "./cli-vdpnctkq.js";
10
10
  import {
11
11
  EXIT_CODES
12
12
  } from "./cli-46ycxe6r.js";
@@ -18,14 +18,14 @@ import {
18
18
  import { defineCommand, parseArgs, runMain } from "citty";
19
19
  import { statSync } from "node:fs";
20
20
  var SUBCOMMANDS = {
21
- new: () => import("./new-y5kn67a5.js").then((m) => m.default),
22
- preview: () => import("./preview-mk98eva0.js").then((m) => m.default),
23
- build: () => import("./build-czwbmbsb.js").then((m) => m.default),
24
- publish: () => import("./publish-q7zpx8yq.js").then((m) => m.default),
25
- validate: () => import("./validate-scqwwwx5.js").then((m) => m.default),
26
- preflight: () => import("./preflight-aj2cps9w.js").then((m) => m.default),
27
- doctor: () => import("./doctor-ej9e88dc.js").then((m) => m.default),
28
- ext: () => import("./ext-47cs7bp8.js").then((m) => m.default)
21
+ new: () => import("./new-7npm260n.js").then((m) => m.default),
22
+ preview: () => import("./preview-xkp190pd.js").then((m) => m.default),
23
+ build: () => import("./build-4522yqmm.js").then((m) => m.default),
24
+ publish: () => import("./publish-czx47htf.js").then((m) => m.default),
25
+ validate: () => import("./validate-x89z3zrq.js").then((m) => m.default),
26
+ preflight: () => import("./preflight-y0jhsz8p.js").then((m) => m.default),
27
+ doctor: () => import("./doctor-fgvwe9j2.js").then((m) => m.default),
28
+ ext: () => import("./ext-v44phkxz.js").then((m) => m.default)
29
29
  };
30
30
  var VERSION = package_default.version;
31
31
  var main = defineCommand({
@@ -50,7 +50,7 @@ async function preflightRequiredInvocations(rawArgs) {
50
50
  const [command, ...commandArgs] = rawArgs;
51
51
  try {
52
52
  if (command === "new") {
53
- const { newArgs } = await import("./new-y5kn67a5.js");
53
+ const { newArgs } = await import("./new-7npm260n.js");
54
54
  rejectUnknownFlags(commandArgs, newArgs, "new");
55
55
  const parsed = parseArgs(commandArgs, {
56
56
  ...newArgs,
@@ -62,7 +62,7 @@ async function preflightRequiredInvocations(rawArgs) {
62
62
  return;
63
63
  }
64
64
  if (command === "preflight") {
65
- const { preflightArgs } = await import("./preflight-aj2cps9w.js");
65
+ const { preflightArgs } = await import("./preflight-y0jhsz8p.js");
66
66
  rejectUnknownFlags(commandArgs, preflightArgs, "preflight");
67
67
  const parsed = parseArgs(commandArgs, {
68
68
  ...preflightArgs,
@@ -82,7 +82,7 @@ async function preflightRequiredInvocations(rawArgs) {
82
82
  }
83
83
  if (subcommand.startsWith("-"))
84
84
  return;
85
- const { EXT_SUBCOMMANDS, extAddArgs, extRemoveArgs, extEnableArgs, extDisableArgs } = await import("./ext-47cs7bp8.js");
85
+ const { EXT_SUBCOMMANDS, extAddArgs, extRemoveArgs, extEnableArgs, extDisableArgs } = await import("./ext-v44phkxz.js");
86
86
  if (!EXT_SUBCOMMANDS.includes(subcommand)) {
87
87
  throw new UsageError(`gutterpress ext: unknown command "${subcommand}"`);
88
88
  }
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  getSystemDiagnostics,
3
3
  log
4
- } from "./cli-7z0j8sqj.js";
4
+ } from "./cli-b496rxy0.js";
5
5
  import {
6
6
  UsageError,
7
7
  rejectExtraPositionals,
8
8
  rejectUnknownFlags
9
- } from "./cli-1723rkd1.js";
9
+ } from "./cli-vdpnctkq.js";
10
10
  import"./cli-46ycxe6r.js";
11
11
  import"./cli-7fkhgtfn.js";
12
12
  import"./cli-37x76zdn.js";
@@ -19,6 +19,8 @@ export interface BuildOptions {
19
19
  input: string;
20
20
  output?: string;
21
21
  signature?: number;
22
+ /** guarantee the final page is a blank page added by postprocess */
23
+ reserveLastPage?: boolean;
22
24
  marks?: boolean;
23
25
  slugPt?: number;
24
26
  bleedPt?: number;
@@ -3,6 +3,12 @@ export interface PostprocessOptions {
3
3
  geometry: PageTrim;
4
4
  /** append blank pages until pageCount % signature === 0 */
5
5
  signature?: number;
6
+ /**
7
+ * Guarantee the last page is a blank page added here. If signature padding
8
+ * would add nothing, append one full signature of blank pages (one page when
9
+ * signature <= 1).
10
+ */
11
+ reserveLastPage?: boolean;
6
12
  title?: string;
7
13
  author?: string;
8
14
  subject?: string;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  build
3
- } from "./cli-tjhn2mdn.js";
3
+ } from "./cli-mc3waxaw.js";
4
4
  import {
5
5
  BuildError
6
6
  } from "./cli-46ycxe6r.js";
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  build
3
- } from "./index-b6p1fh61.js";
3
+ } from "./index-ypxhxvwv.js";
4
4
  import {
5
5
  BuildError
6
- } from "./index-mw8b25yx.js";
6
+ } from "./index-w6tqhafk.js";
7
7
  import"./index-rzm1phg0.js";
8
8
  import"./index-37x76zdn.js";
9
9
 
@@ -12,16 +12,16 @@ import {
12
12
  searchNpmExtensions,
13
13
  setExtensionEnabled,
14
14
  updateExtensions
15
- } from "./cli-vgjtz2e6.js";
16
- import"./cli-7z0j8sqj.js";
15
+ } from "./cli-7pw53gnm.js";
16
+ import"./cli-b496rxy0.js";
17
17
  import {
18
18
  UsageError,
19
19
  exitForUsage,
20
20
  rejectExtraPositionals,
21
21
  rejectUnknownFlags,
22
22
  resolveProjectDir
23
- } from "./cli-1723rkd1.js";
24
- import"./cli-tjhn2mdn.js";
23
+ } from "./cli-vdpnctkq.js";
24
+ import"./cli-mc3waxaw.js";
25
25
  import {
26
26
  EXIT_CODES
27
27
  } from "./cli-46ycxe6r.js";
@@ -4,7 +4,7 @@ import {
4
4
  MARGIN_BOX_IGNORED_PROPERTIES,
5
5
  getAssetPath,
6
6
  getAssetsDir
7
- } from "./index-mw8b25yx.js";
7
+ } from "./index-w6tqhafk.js";
8
8
  import {
9
9
  INSTALL_HINTS,
10
10
  execCapture,
@@ -140,11 +140,12 @@ var DTRPG_PRESET = {
140
140
  source: {
141
141
  files: null
142
142
  },
143
- print: { signature: 1 },
143
+ print: { signature: 4, reserveLastPage: true },
144
144
  pdfx: {
145
145
  flavor: "x1a",
146
146
  icc: "profiles/CGATS21_CRPC1.icc",
147
- stripAnnotations: true
147
+ stripAnnotations: true,
148
+ blackText: "k-only"
148
149
  },
149
150
  page: {
150
151
  width: 621,
@@ -214,11 +215,12 @@ var BOOK_PRESET = {
214
215
  source: {
215
216
  files: null
216
217
  },
217
- print: { signature: 1 },
218
+ print: { signature: 1, reserveLastPage: false },
218
219
  pdfx: {
219
220
  flavor: "x1a",
220
221
  icc: "profiles/CGATS21_CRPC1.icc",
221
- stripAnnotations: true
222
+ stripAnnotations: true,
223
+ blackText: "icc"
222
224
  },
223
225
  page: {
224
226
  width: 432,
@@ -3858,7 +3860,7 @@ import git from "isomorphic-git";
3858
3860
  // package.json
3859
3861
  var package_default = {
3860
3862
  name: "gutterpress",
3861
- version: "0.11.14",
3863
+ version: "0.11.15-alpha.2",
3862
3864
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
3863
3865
  author: "itlackey",
3864
3866
  license: "MPL-2.0",
@@ -4123,6 +4125,261 @@ async function writeBuildFingerprint(input) {
4123
4125
  return outPath;
4124
4126
  }
4125
4127
 
4128
+ // src/lib/black-text-gray.ts
4129
+ var MAX_CHANNEL = 0.12;
4130
+ var MAX_SPREAD = 0.04;
4131
+ function isNearBlack([r, g, b]) {
4132
+ const max = Math.max(r, g, b);
4133
+ return max <= MAX_CHANNEL && max - Math.min(r, g, b) <= MAX_SPREAD;
4134
+ }
4135
+ var WS = new Set([0, 9, 10, 12, 13, 32]);
4136
+ var DELIM = new Set("()<>[]{}/%".split("").map((c) => c.charCodeAt(0)));
4137
+ function* tokenize(s) {
4138
+ let i = 0;
4139
+ const n = s.length;
4140
+ while (i < n) {
4141
+ const c = s.charCodeAt(i);
4142
+ if (WS.has(c)) {
4143
+ i++;
4144
+ continue;
4145
+ }
4146
+ const start = i;
4147
+ if (c === 37) {
4148
+ while (i < n && s[i] !== `
4149
+ ` && s[i] !== "\r")
4150
+ i++;
4151
+ continue;
4152
+ }
4153
+ if (c === 40) {
4154
+ let depth = 0;
4155
+ while (i < n) {
4156
+ const d = s[i++];
4157
+ if (d === "\\")
4158
+ i++;
4159
+ else if (d === "(")
4160
+ depth++;
4161
+ else if (d === ")" && --depth === 0)
4162
+ break;
4163
+ }
4164
+ yield { text: s.slice(start, i), start, end: i, op: false };
4165
+ continue;
4166
+ }
4167
+ if (c === 60 && s[i + 1] === "<") {
4168
+ i += 2;
4169
+ yield { text: "<<", start, end: i, op: false };
4170
+ continue;
4171
+ }
4172
+ if (c === 62 && s[i + 1] === ">") {
4173
+ i += 2;
4174
+ yield { text: ">>", start, end: i, op: false };
4175
+ continue;
4176
+ }
4177
+ if (c === 60) {
4178
+ while (i < n && s[i] !== ">")
4179
+ i++;
4180
+ i++;
4181
+ yield { text: s.slice(start, i), start, end: i, op: false };
4182
+ continue;
4183
+ }
4184
+ if (c === 91 || c === 93 || c === 62 || c === 123 || c === 125 || c === 41) {
4185
+ i++;
4186
+ yield { text: s[start], start, end: i, op: false };
4187
+ continue;
4188
+ }
4189
+ i++;
4190
+ while (i < n && !WS.has(s.charCodeAt(i)) && !DELIM.has(s.charCodeAt(i)))
4191
+ i++;
4192
+ const text = s.slice(start, i);
4193
+ const isOperand = c === 47 || /^[+-]?(\d+\.?\d*|\.\d+)$/.test(text);
4194
+ yield { text, start, end: i, op: !isOperand };
4195
+ if (text === "ID") {
4196
+ const m = /\sEI(?=\s|$)/g;
4197
+ m.lastIndex = i;
4198
+ const hit = m.exec(s);
4199
+ i = hit ? hit.index + hit[0].length : n;
4200
+ }
4201
+ }
4202
+ }
4203
+ function rewriteBlackText(src) {
4204
+ let fill = null;
4205
+ let stroke = null;
4206
+ const stack = [];
4207
+ let inText = false;
4208
+ let fillDirty = false;
4209
+ let strokeDirty = false;
4210
+ const edits = [];
4211
+ let operands = [];
4212
+ const rgbOf = () => {
4213
+ if (operands.length !== 3)
4214
+ return null;
4215
+ const v = operands.map((t) => Number(t.text));
4216
+ return v.some((x) => Number.isNaN(x)) ? null : { rgb: v, raw: operands.map((t) => t.text).join(" ") };
4217
+ };
4218
+ for (const t of tokenize(src)) {
4219
+ if (!t.op) {
4220
+ operands.push(t);
4221
+ continue;
4222
+ }
4223
+ const op = t.text;
4224
+ switch (op) {
4225
+ case "q":
4226
+ stack.push([fill, stroke]);
4227
+ break;
4228
+ case "Q": {
4229
+ const p = stack.pop();
4230
+ if (p)
4231
+ [fill, stroke] = p;
4232
+ break;
4233
+ }
4234
+ case "BT":
4235
+ inText = true;
4236
+ fillDirty = strokeDirty = false;
4237
+ {
4238
+ let ins = "";
4239
+ if (fill && isNearBlack(fill.rgb)) {
4240
+ ins += " 0 g";
4241
+ fillDirty = true;
4242
+ }
4243
+ if (stroke && isNearBlack(stroke.rgb)) {
4244
+ ins += " 0 G";
4245
+ strokeDirty = true;
4246
+ }
4247
+ if (ins)
4248
+ edits.push({ start: t.end, end: t.end, text: ins });
4249
+ }
4250
+ break;
4251
+ case "ET":
4252
+ if (inText) {
4253
+ let ins = "";
4254
+ if (fillDirty && fill)
4255
+ ins += `${fill.raw} rg `;
4256
+ if (strokeDirty && stroke)
4257
+ ins += `${stroke.raw} RG `;
4258
+ if (ins)
4259
+ edits.push({ start: t.start, end: t.start, text: ins });
4260
+ }
4261
+ inText = false;
4262
+ break;
4263
+ case "rg":
4264
+ case "RG": {
4265
+ const c = rgbOf();
4266
+ const isFill = op === "rg";
4267
+ if (isFill)
4268
+ fill = c;
4269
+ else
4270
+ stroke = c;
4271
+ if (inText && c && isNearBlack(c.rgb)) {
4272
+ edits.push({ start: operands[0].start, end: t.end, text: isFill ? "0 g" : "0 G" });
4273
+ if (isFill)
4274
+ fillDirty = true;
4275
+ else
4276
+ strokeDirty = true;
4277
+ } else if (inText) {
4278
+ if (isFill)
4279
+ fillDirty = false;
4280
+ else
4281
+ strokeDirty = false;
4282
+ }
4283
+ break;
4284
+ }
4285
+ case "g":
4286
+ case "k":
4287
+ case "sc":
4288
+ case "scn":
4289
+ case "cs":
4290
+ fill = null;
4291
+ if (inText)
4292
+ fillDirty = false;
4293
+ break;
4294
+ case "G":
4295
+ case "K":
4296
+ case "SC":
4297
+ case "SCN":
4298
+ case "CS":
4299
+ stroke = null;
4300
+ if (inText)
4301
+ strokeDirty = false;
4302
+ break;
4303
+ }
4304
+ operands = [];
4305
+ }
4306
+ return applyEdits(src, edits);
4307
+ }
4308
+ function applyEdits(s, edits) {
4309
+ if (!edits.length)
4310
+ return s;
4311
+ let out = "";
4312
+ let pos = 0;
4313
+ for (const e of edits) {
4314
+ out += s.slice(pos, e.start) + e.text;
4315
+ pos = e.end;
4316
+ }
4317
+ return out + s.slice(pos);
4318
+ }
4319
+ async function blackTextToGray(pdf) {
4320
+ const { PDFDocument, PDFName, PDFRawStream, PDFArray, PDFRef, decodePDFRawStream } = await import("pdf-lib");
4321
+ const doc = await PDFDocument.load(pdf, { updateMetadata: false });
4322
+ const ctx = doc.context;
4323
+ const SKIP = ["Filter", "DecodeParms", "Length"].map((k) => PDFName.of(k));
4324
+ let changed = false;
4325
+ const decode = (s) => {
4326
+ try {
4327
+ return Buffer.from(decodePDFRawStream(s).decode()).toString("latin1");
4328
+ } catch {
4329
+ return null;
4330
+ }
4331
+ };
4332
+ const replace = (ref, old, text) => {
4333
+ const entries = {};
4334
+ for (const [k, v] of old.dict.entries())
4335
+ if (!SKIP.includes(k))
4336
+ entries[k.decodeText()] = v;
4337
+ ctx.assign(ref, ctx.flateStream(Buffer.from(text, "latin1"), entries));
4338
+ changed = true;
4339
+ };
4340
+ for (const [ref, obj] of ctx.enumerateIndirectObjects()) {
4341
+ if (!(obj instanceof PDFRawStream))
4342
+ continue;
4343
+ if (obj.dict.get(PDFName.of("Subtype")) !== PDFName.of("Form"))
4344
+ continue;
4345
+ const text = decode(obj);
4346
+ if (text === null)
4347
+ continue;
4348
+ const next = rewriteBlackText(text);
4349
+ if (next !== text)
4350
+ replace(ref, obj, next);
4351
+ }
4352
+ for (const page of doc.getPages()) {
4353
+ const contents = page.node.Contents();
4354
+ const refs = [];
4355
+ if (contents instanceof PDFArray) {
4356
+ for (let i = 0;i < contents.size(); i++) {
4357
+ const r = contents.get(i);
4358
+ if (r instanceof PDFRef)
4359
+ refs.push(r);
4360
+ }
4361
+ } else if (contents instanceof PDFRawStream) {
4362
+ const r = page.node.get(PDFName.of("Contents"));
4363
+ if (r instanceof PDFRef)
4364
+ refs.push(r);
4365
+ }
4366
+ const streams = refs.map((r) => ctx.lookup(r));
4367
+ if (!refs.length || !streams.every((s) => s instanceof PDFRawStream))
4368
+ continue;
4369
+ const parts = streams.map((s) => decode(s));
4370
+ if (parts.some((p) => p === null))
4371
+ continue;
4372
+ const joined = parts.join(`
4373
+ `);
4374
+ const next = rewriteBlackText(joined);
4375
+ if (next === joined)
4376
+ continue;
4377
+ replace(refs[0], streams[0], next);
4378
+ page.node.set(PDFName.of("Contents"), refs[0]);
4379
+ }
4380
+ return changed ? doc.save({ updateFieldAppearances: false }) : pdf;
4381
+ }
4382
+
4126
4383
  // src/lib/validation-exec.ts
4127
4384
  import { existsSync as existsSync10 } from "node:fs";
4128
4385
  import { dirname as dirname4, join as join4, resolve as resolve8 } from "node:path";
@@ -8218,11 +8475,12 @@ class PdfOutput {
8218
8475
  const rawPdf = pdfxMode ? path8.join(stage, "raw.pdf") : path8.resolve(pdfFile);
8219
8476
  await fsp2.mkdir(path8.dirname(path8.resolve(pdfFile)), { recursive: true });
8220
8477
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
8221
- const { buildNativePdf } = await import("./engine-bgd4aa99.js");
8478
+ const { buildNativePdf } = await import("./engine-s18wy689.js");
8222
8479
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
8223
8480
  title: config.title,
8224
8481
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
8225
8482
  signature: config.print.signature,
8483
+ reserveLastPage: config.print.reserveLastPage,
8226
8484
  allowShrink: opts.allowShrink
8227
8485
  }, browser);
8228
8486
  for (const d of engineDiagnostics)
@@ -8248,6 +8506,10 @@ class PdfOutput {
8248
8506
  log.info("Stripping annotations for PDF/X compliance");
8249
8507
  await stripAnnotations(rawPdf, stage);
8250
8508
  }
8509
+ if (config.pdfx.blackText === "k-only") {
8510
+ log.info("Rewriting near-black text to K-only gray");
8511
+ await fsp2.writeFile(rawPdf, await blackTextToGray(await fsp2.readFile(rawPdf)));
8512
+ }
8251
8513
  log.info(`Converting to CMYK PDF/X (${pdfxMode})`);
8252
8514
  await convertToPdfxCmyk(rawPdf, path8.resolve(pdfFile), {
8253
8515
  iccPath: effectiveIccPath,
@@ -26,7 +26,7 @@ import { fileURLToPath } from "node:url";
26
26
  var favicon_default = "./favicon-wkbm9cjn.ico";
27
27
 
28
28
  // src/assets/manifest.schema.json
29
- var manifest_schema_default = "./manifest.schema-61bsspsb.json";
29
+ var manifest_schema_default = "./manifest.schema-40sfq49z.json";
30
30
 
31
31
  // src/assets/preview/scripts/preview-interface.js
32
32
  var preview_interface_default = "./preview-interface-twd6nsf5.js";
@@ -3,7 +3,7 @@ import {
3
3
  isIgnoredMarginBoxProperty,
4
4
  marginBoxAlign,
5
5
  marginBoxRectPt
6
- } from "./index-mw8b25yx.js";
6
+ } from "./index-w6tqhafk.js";
7
7
  import {
8
8
  launchChromium
9
9
  } from "./index-rzm1phg0.js";
@@ -1207,15 +1207,12 @@ async function postprocess(input, opts) {
1207
1207
  g.media.width - inset,
1208
1208
  g.media.height - inset
1209
1209
  ];
1210
- let padded = 0;
1211
- if (opts.signature && opts.signature > 1) {
1212
- const remainder = doc.getPageCount() % opts.signature;
1213
- if (remainder !== 0) {
1214
- padded = opts.signature - remainder;
1215
- for (let i = 0;i < padded; i++)
1216
- doc.addPage([g.media.width, g.media.height]);
1217
- }
1218
- }
1210
+ const signature = opts.signature && opts.signature > 1 ? opts.signature : 1;
1211
+ let padded = (signature - doc.getPageCount() % signature) % signature;
1212
+ if (padded === 0 && opts.reserveLastPage)
1213
+ padded = signature;
1214
+ for (let i = 0;i < padded; i++)
1215
+ doc.addPage([g.media.width, g.media.height]);
1219
1216
  const wantMarks = opts.marks || g.marks.includes("crop") || g.marks.includes("cross");
1220
1217
  for (const page of doc.getPages()) {
1221
1218
  const node = page.node;
@@ -2002,11 +1999,12 @@ ${mapCss}`;
2002
1999
  const post = await postprocess(bytes, {
2003
2000
  geometry: tier2.geometry,
2004
2001
  signature: opts.signature,
2002
+ reserveLastPage: opts.reserveLastPage,
2005
2003
  marks: opts.marks,
2006
2004
  title: opts.title,
2007
2005
  author: opts.author
2008
2006
  });
2009
- log(`postprocess: ${post.pageCount} pages` + (post.padded ? ` (+${post.padded} signature pad)` : ""));
2007
+ log(`postprocess: ${post.pageCount} pages` + (post.padded ? ` (+${post.padded} blank${opts.reserveLastPage ? "; last page reserved blank" : ""})` : ""));
2010
2008
  return {
2011
2009
  bytes: post.bytes,
2012
2010
  tier,
package/dist/index.js CHANGED
@@ -146,13 +146,13 @@ import {
146
146
  testRemoteAccess,
147
147
  updateExtensions,
148
148
  validateProjectExtensions
149
- } from "./index-nm81wpfb.js";
149
+ } from "./index-j77039xr.js";
150
150
  import {
151
151
  BUILD_DIAGNOSTIC_CODES
152
- } from "./index-b6p1fh61.js";
152
+ } from "./index-ypxhxvwv.js";
153
153
  import {
154
154
  BuildError
155
- } from "./index-mw8b25yx.js";
155
+ } from "./index-w6tqhafk.js";
156
156
  import {
157
157
  DEFAULT_PRINT_OPTS,
158
158
  assertMilestone,
@@ -0,0 +1,40 @@
1
+ /**
2
+ * K-only black text for PDF/X (#331).
3
+ *
4
+ * PRINT-SPEC GAP: print vendors (DriveThruRPG) require body text to print on
5
+ * the black plate alone (100% K). Chromium writes every text fill as
6
+ * DeviceRGB (`r g b rg`), and the Ghostscript pass in `convertToPdfxCmyk`
7
+ * sends all RGB through the output ICC, so `#000` text lands as four-colour
8
+ * rich black (~47/42/40/100). Ghostscript keeps DeviceGray as gray, which
9
+ * maps to K only — so before that pass, near-black RGB that paints TEXT is
10
+ * rewritten to DeviceGray black. Standard PDF operators in, standard PDF
11
+ * operators out; nothing here is engine-private.
12
+ *
13
+ * REMOVAL TRIGGER: Chromium emitting DeviceGray/DeviceCMYK for black text, or
14
+ * Ghostscript gaining a text-only RGB-black → K mapping (`-dBlackText` and
15
+ * `Text_RGB None` do not do this, measured).
16
+ *
17
+ * What Chromium emits (measured): colour is set with one `r g b RG r g b rg`
18
+ * line directly BEFORE a `BT` block and simply stays in effect afterwards, so
19
+ * the same colour also paints whatever follows `ET` (rules, text-decoration
20
+ * rectangles). Those non-text fills must keep their colour. So the rewrite is
21
+ * scoped to `BT`…`ET`: on entering a text object whose inherited fill/stroke
22
+ * is near-black RGB we emit `0 g`/`0 G`, and at `ET` we put the original RGB
23
+ * back. A near-black `rg`/`RG` set inside the block is rewritten the same way.
24
+ * Page content streams and Form XObjects are both processed; a Form's
25
+ * inherited (pre-`Do`) colour is unknowable from its own stream, so only
26
+ * colours set inside the form are considered.
27
+ *
28
+ * Known limit: text-decoration rectangles (underline/strike) are `re f` after
29
+ * `ET`, i.e. non-text painting, and stay rich black.
30
+ */
31
+ /**
32
+ * Rewrite near-black RGB fill/stroke to DeviceGray black inside BT…ET of one
33
+ * content stream. Pure string → string; returns the input when nothing changed.
34
+ */
35
+ export declare function rewriteBlackText(src: string): string;
36
+ /**
37
+ * Apply {@link rewriteBlackText} to every page content stream and Form XObject
38
+ * of a PDF. Returns the input bytes untouched when nothing needed rewriting.
39
+ */
40
+ export declare function blackTextToGray(pdf: Uint8Array): Promise<Uint8Array>;
@@ -6,6 +6,7 @@ export interface NativePdfOptions {
6
6
  title?: string;
7
7
  author?: string;
8
8
  signature?: number;
9
+ reserveLastPage?: boolean;
9
10
  /**
10
11
  * Downgrade the engine's over-wide-content hard error to a warning +
11
12
  * diagnostic. The engine's message tells the author to "pass allowShrink to
@@ -148,6 +148,28 @@
148
148
  "type": "boolean",
149
149
  "description": "Strip annotations (links, comments) during PDF/X conversion — PDF/X-1a does not permit them.",
150
150
  "default": true
151
+ },
152
+ "blackText": {
153
+ "type": "string",
154
+ "description": "How near-black text is separated. `icc` converts it through the ICC profile (rich, four-colour black). `k-only` prints it on the black plate alone (100% K), which print vendors such as DriveThruRPG require for small text. Non-text black (rules, panels) is unaffected. Preset defaults: dtrpg `k-only`, book `icc`.",
155
+ "enum": ["icc", "k-only"],
156
+ "default": "icc"
157
+ }
158
+ }
159
+ },
160
+ "print": {
161
+ "type": "object",
162
+ "description": "Page-count rules applied when the PDF is built.",
163
+ "properties": {
164
+ "signature": {
165
+ "type": "integer",
166
+ "description": "Pad the PDF with blank pages until the page count is a multiple of this. `1` means no padding. Preset defaults: dtrpg 4, book 1.",
167
+ "minimum": 1,
168
+ "examples": [1, 4, 6]
169
+ },
170
+ "reserveLastPage": {
171
+ "type": "boolean",
172
+ "description": "Guarantee the final page is a blank page added by the build (no background, no page number). When the content already fills a signature exactly, a full signature of blank pages is added. DriveThruRPG reserves the last page for printer information. Preset defaults: dtrpg true, book false."
151
173
  }
152
174
  }
153
175
  },
@@ -7,16 +7,16 @@ import {
7
7
  TARGET_IDS,
8
8
  scaffoldExtension,
9
9
  scaffoldProject
10
- } from "./cli-vgjtz2e6.js";
10
+ } from "./cli-7pw53gnm.js";
11
11
  import {
12
12
  resolveGhostscript
13
- } from "./cli-7z0j8sqj.js";
13
+ } from "./cli-b496rxy0.js";
14
14
  import {
15
15
  UsageError,
16
16
  rejectExtraPositionals,
17
17
  rejectUnknownFlags
18
- } from "./cli-1723rkd1.js";
19
- import"./cli-tjhn2mdn.js";
18
+ } from "./cli-vdpnctkq.js";
19
+ import"./cli-mc3waxaw.js";
20
20
  import {
21
21
  EXIT_CODES
22
22
  } from "./cli-46ycxe6r.js";
@@ -2,16 +2,16 @@ import {
2
2
  executeValidation,
3
3
  publishTargetFor,
4
4
  reportMissingTools
5
- } from "./cli-vgjtz2e6.js";
5
+ } from "./cli-7pw53gnm.js";
6
6
  import {
7
7
  log
8
- } from "./cli-7z0j8sqj.js";
8
+ } from "./cli-b496rxy0.js";
9
9
  import {
10
10
  UsageError,
11
11
  rejectExtraPositionals,
12
12
  rejectUnknownFlags
13
- } from "./cli-1723rkd1.js";
14
- import"./cli-tjhn2mdn.js";
13
+ } from "./cli-vdpnctkq.js";
14
+ import"./cli-mc3waxaw.js";
15
15
  import {
16
16
  EXIT_CODES
17
17
  } from "./cli-46ycxe6r.js";
@@ -6,10 +6,10 @@ import {
6
6
  runBuild,
7
7
  splitOutPath,
8
8
  startPreviewServer
9
- } from "./cli-vgjtz2e6.js";
9
+ } from "./cli-7pw53gnm.js";
10
10
  import {
11
11
  log
12
- } from "./cli-7z0j8sqj.js";
12
+ } from "./cli-b496rxy0.js";
13
13
  import {
14
14
  UsageError,
15
15
  parseFormat,
@@ -17,8 +17,8 @@ import {
17
17
  rejectExtraPositionals,
18
18
  rejectUnknownFlags,
19
19
  resolvePort
20
- } from "./cli-1723rkd1.js";
21
- import"./cli-tjhn2mdn.js";
20
+ } from "./cli-vdpnctkq.js";
21
+ import"./cli-mc3waxaw.js";
22
22
  import {
23
23
  BuildError
24
24
  } from "./cli-46ycxe6r.js";
@@ -11,17 +11,17 @@ import {
11
11
  publishProviderFor,
12
12
  resolvePublishFormat,
13
13
  runPublish
14
- } from "./cli-vgjtz2e6.js";
14
+ } from "./cli-7pw53gnm.js";
15
15
  import {
16
16
  FileTokenStore,
17
17
  log
18
- } from "./cli-7z0j8sqj.js";
18
+ } from "./cli-b496rxy0.js";
19
19
  import {
20
20
  UsageError,
21
21
  rejectExtraPositionals,
22
22
  rejectUnknownFlags
23
- } from "./cli-1723rkd1.js";
24
- import"./cli-tjhn2mdn.js";
23
+ } from "./cli-vdpnctkq.js";
24
+ import"./cli-mc3waxaw.js";
25
25
  import {
26
26
  EXIT_CODES
27
27
  } from "./cli-46ycxe6r.js";
@@ -115,6 +115,8 @@ export interface GutterpressManifest {
115
115
  flavor?: "x1a" | "x3";
116
116
  icc?: string;
117
117
  stripAnnotations?: boolean;
118
+ /** `k-only` prints near-black text on the black plate alone (#331). */
119
+ blackText?: "icc" | "k-only";
118
120
  };
119
121
  /**
120
122
  * Native-engine print-production options with no other manifest home
@@ -124,6 +126,12 @@ export interface GutterpressManifest {
124
126
  print?: {
125
127
  /** Pad the PDF with blank pages until pageCount is a multiple of this. */
126
128
  signature?: number;
129
+ /**
130
+ * Guarantee the interior's final page is a blank page appended by the
131
+ * build (no content, no folio). Print-on-demand services such as
132
+ * DriveThruRPG reserve the last page for printer info.
133
+ */
134
+ reserveLastPage?: boolean;
127
135
  };
128
136
  page?: {
129
137
  width?: number;
@@ -227,9 +235,11 @@ export interface ResolvedConfig {
227
235
  flavor: "x1a" | "x3";
228
236
  icc: string;
229
237
  stripAnnotations: boolean;
238
+ blackText: "icc" | "k-only";
230
239
  };
231
240
  print: {
232
241
  signature: number;
242
+ reserveLastPage: boolean;
233
243
  };
234
244
  page: {
235
245
  width: number;
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  executeAndReport
3
- } from "./cli-vgjtz2e6.js";
3
+ } from "./cli-7pw53gnm.js";
4
4
  import {
5
5
  log
6
- } from "./cli-7z0j8sqj.js";
6
+ } from "./cli-b496rxy0.js";
7
7
  import {
8
8
  UsageError,
9
9
  rejectExtraPositionals,
10
10
  rejectUnknownFlags
11
- } from "./cli-1723rkd1.js";
12
- import"./cli-tjhn2mdn.js";
11
+ } from "./cli-vdpnctkq.js";
12
+ import"./cli-mc3waxaw.js";
13
13
  import {
14
14
  EXIT_CODES
15
15
  } from "./cli-46ycxe6r.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gutterpress",
3
- "version": "0.11.14",
3
+ "version": "0.11.15-alpha.2",
4
4
  "description": "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
5
5
  "author": "itlackey",
6
6
  "license": "MPL-2.0",