gutterpress 0.11.20-alpha.2 → 0.11.20-alpha.4

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 (43) hide show
  1. package/dist/api/index.js +3 -2
  2. package/dist/{build-qmjg9r2d.js → build-epdcrm2c.js} +5 -4
  3. package/dist/{cli-zacevwcy.js → cli-0ey902sx.js} +267 -455
  4. package/dist/{cli-j802s31j.js → cli-61vd4ews.js} +83 -41
  5. package/dist/{cli-7z29z4wz.js → cli-fmksazej.js} +1 -1
  6. package/dist/{cli-gh0ym80r.js → cli-h4tfmpg3.js} +121 -20
  7. package/dist/cli-sth219v1.js +418 -0
  8. package/dist/cli.js +12 -12
  9. package/dist/{doctor-fe7tcs4v.js → doctor-78es5scm.js} +2 -2
  10. package/dist/engine/compiler/build.d.ts +1 -1
  11. package/dist/engine/shared/margin-box-support.d.ts +2 -2
  12. package/dist/engine/shared/pin-edges.d.ts +127 -0
  13. package/dist/engine/shared/print-area.d.ts +68 -0
  14. package/dist/{engine-9xb1ky8d.js → engine-67f68bxs.js} +1 -1
  15. package/dist/{engine-yax79k9m.js → engine-7ha27n2b.js} +2 -2
  16. package/dist/{ext-ahwxt6gv.js → ext-9mc7sr0d.js} +5 -4
  17. package/dist/{gutterpress-agent-cazqstr1.js → gutterpress-agent-mrdnwktc.js} +78 -13
  18. package/dist/{gutterpress-viewer-retj6ym2.js → gutterpress-viewer-jr30yw8t.js} +155 -56
  19. package/dist/{index-6xwtb7cw.js → index-ad95fws8.js} +82 -40
  20. package/dist/{index-3nr5rzzp.js → index-gm5xbx5w.js} +3 -3
  21. package/dist/index-sth219v1.js +418 -0
  22. package/dist/{index-razpty47.js → index-t13p8y08.js} +493 -582
  23. package/dist/index.js +4 -3
  24. package/dist/lib/build-fingerprint.d.ts +8 -0
  25. package/dist/lib/extension-manifest.d.ts +13 -0
  26. package/dist/lib/ghostscript.d.ts +70 -16
  27. package/dist/lib/markdown/assemble.d.ts +12 -2
  28. package/dist/lib/markdown/gutterpress-css.d.ts +46 -30
  29. package/dist/lib/markdown/index.d.ts +5 -0
  30. package/dist/lib/pdf-inspect.d.ts +45 -0
  31. package/dist/{manifest.schema-74s1ykyv.json → manifest.schema-13dfax80.json} +16 -4
  32. package/dist/{new-ftd4fec4.js → new-s4f0tnya.js} +5 -4
  33. package/dist/pdf-inspect-zdmy7qca.js +35 -0
  34. package/dist/{preflight-8sqarcsr.js → preflight-7ck4rbsq.js} +5 -4
  35. package/dist/preview/file-watcher.d.ts +4 -10
  36. package/dist/{preview-q76xn95h.js → preview-j91sxadd.js} +5 -4
  37. package/dist/{publish-7gdy6gbf.js → publish-f12xaenm.js} +5 -4
  38. package/dist/render.js +57 -6
  39. package/dist/schema/manifest.types.d.ts +28 -8
  40. package/dist/test-helpers/testkit.d.ts +2 -2
  41. package/dist/{validate-e3ryegce.js → validate-pb5gysvr.js} +5 -4
  42. package/package.json +1 -1
  43. package/dist/engine/shared/flush.d.ts +0 -59
package/dist/index.js CHANGED
@@ -154,14 +154,15 @@ import {
154
154
  testRemoteAccess,
155
155
  updateExtensions,
156
156
  validateProjectExtensions
157
- } from "./index-razpty47.js";
157
+ } from "./index-t13p8y08.js";
158
158
  import"./index-ht1bcqdd.js";
159
159
  import {
160
160
  BUILD_DIAGNOSTIC_CODES
161
- } from "./index-6xwtb7cw.js";
161
+ } from "./index-ad95fws8.js";
162
162
  import {
163
163
  BuildError
164
- } from "./index-3nr5rzzp.js";
164
+ } from "./index-gm5xbx5w.js";
165
+ import"./index-sth219v1.js";
165
166
  import {
166
167
  DEFAULT_PRINT_OPTS,
167
168
  assertMilestone,
@@ -5,6 +5,14 @@ type PdfxFingerprintConfig = {
5
5
  stripAnnotations: boolean | null;
6
6
  /** How near-black text was separated (#331); null when no PDF/X pass ran. Provenance only. */
7
7
  blackText?: "icc" | "k-only" | null;
8
+ /**
9
+ * How pages with transparency were handled (#385): `pdfx.transparency`,
10
+ * `pdfx.flattenPages` and `pdfx.rasterDpi`. null when no PDF/X pass ran
11
+ * (and, for rasterDpi, when unset). Provenance only.
12
+ */
13
+ transparency?: "flatten-page" | "preserve-text" | null;
14
+ flattenPages?: number[] | null;
15
+ rasterDpi?: number | null;
8
16
  };
9
17
  export type BuildFingerprintInput = {
10
18
  command: "build";
@@ -70,6 +70,12 @@ export interface ExtensionMetadata {
70
70
  * does the merge.
71
71
  */
72
72
  snippets?: string;
73
+ /**
74
+ * npm `engines.gutterpress` — the Gutterpress versions the extension works
75
+ * with, as a semver range (e.g. `">=0.11.20-alpha.3"`). Checked when the
76
+ * extension loads ({@link assertSupportsThisGutterpress}).
77
+ */
78
+ gutterpressRange?: string;
73
79
  /**
74
80
  * The removed `engineStyles` field (#266), carried through from the
75
81
  * `gutterpress` block ONLY so {@link assertExtensionContained} can reject it
@@ -119,6 +125,13 @@ export declare function extensionCarries(meta: ExtensionMetadata): ExtensionCarr
119
125
  * doctrine {@link readExtensionMeta}'s tolerant JSON parse already follows).
120
126
  */
121
127
  export declare function pathEscapesFolder(rel: string): boolean;
128
+ /**
129
+ * Refuse to load an extension whose `engines.gutterpress` range excludes this
130
+ * Gutterpress, so a book built with too old a version fails with the fix
131
+ * instead of quietly printing differently. Prereleases count: 0.12.0-alpha.1
132
+ * satisfies `>=0.11.20`.
133
+ */
134
+ export declare function assertSupportsThisGutterpress(meta: ExtensionMetadata, ref: string): void;
122
135
  /**
123
136
  * Every path an extension declares must live INSIDE its own folder — an
124
137
  * extension is self-contained by contract (apply/install copies the whole
@@ -1,3 +1,4 @@
1
+ import type { UnsplittableReason } from "./pdf-inspect";
1
2
  /**
2
3
  * Resolve the Ghostscript command on every supported platform.
3
4
  *
@@ -8,38 +9,39 @@
8
9
  */
9
10
  export declare function resolveGhostscript(targetPlatform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): Promise<string | undefined>;
10
11
  /**
11
- * Whether `pdfBytes` uses any live PDF transparency — the PDF 1.4 imaging
12
- * feature, in ALL the forms Chromium emits it, not just alpha-channel images:
12
+ * The 1-based page numbers of `pdfBytes` that use live PDF transparency — the
13
+ * PDF 1.4 imaging feature, in ALL the forms Chromium emits it, not just
14
+ * alpha-channel images:
13
15
  *
14
16
  * - a transparency **group** (`/Group << /S /Transparency >>`), which is what
15
17
  * CSS `opacity`, `mix-blend-mode` and friends compile down to;
16
18
  * - an **image soft mask** (`/SMask` on an image XObject, pointing at a real
17
19
  * mask rather than the literal `/None` an opaque image also carries) — an
18
20
  * RGBA source image;
19
- * - a **graphics-state** soft mask or constant alpha (`/SMask`, `/ca`, `/CA`
20
- * on an `/ExtGState`).
21
+ * - a **graphics-state** soft mask, constant alpha (`/ca`, `/CA` below 1) or
22
+ * non-Normal blend mode (`/BM`) on an `/ExtGState` — soft `rgba()`
23
+ * shadows and fills.
21
24
  *
22
25
  * PDF/X-1a and PDF/X-3 (as this codebase generates them, `-dCompatibilityLevel=1.3`
23
26
  * in {@link convertToPdfxCmyk}) are both based on PDF 1.3, which predates the
24
27
  * PDF 1.4 transparency model entirely — live transparency has nothing a
25
28
  * PDF-1.3 file can represent it with. Ghostscript's only PDF/X-compliant
26
29
  * option is to flatten the page it appears ON down to a single raster image
27
- * (B.10, measured: a book with alpha-channel artwork loses every embedded
28
- * font and all searchable text in its PDF/X output, identically on both
29
- * engines — this is a Ghostscript/PDF-1.3 property, not an engine bug).
30
- * Detecting it lets the build warn the author PRECISELY, instead of them
31
- * discovering a fontless PDF/X file after the fact.
30
+ * (B.10, measured: text and fonts on that page are lost, identically on both
31
+ * engines — a Ghostscript/PDF-1.3 property, not an engine bug).
32
+ * {@link flattenTransparentPages} (opt-in, `pdfx.transparency: preserve-text`)
33
+ * can instead keep the text live on the pages this returns.
32
34
  *
33
- * This walks the parsed object graph rather than scanning raw bytes. A byte
34
- * scan is NOT sufficient and produced a false "all clear" in review: image
35
+ * This walks each page's parsed object graph rather than scanning raw bytes. A
36
+ * byte scan is NOT sufficient and produced a false "all clear" in review: image
35
37
  * XObjects are streams, so their dicts do sit uncompressed at top level, but
36
38
  * `/ExtGState` and `/Group` dicts are plain objects that Chromium packs into
37
39
  * **object streams** (`/Type /ObjStm`, deflate-compressed), where no regex can
38
- * see them. Measured on four books with known Ghostscript outcomes, this
39
- * predicate is exactly right on all four; the byte scan silently missed the
40
- * CSS-`opacity` book, which rasterizes to zero embedded fonts.
40
+ * see them. Fonts, `/Parent` (which leads to every other page) and `/Annots`
41
+ * are not followed: an annotation's appearance stream is not page content, and
42
+ * PDF/X builds strip annotations before conversion.
41
43
  */
42
- export declare function hasLiveTransparency(pdfBytes: Uint8Array): Promise<boolean>;
44
+ export declare function transparentPages(pdfBytes: Uint8Array): Promise<number[]>;
43
45
  /**
44
46
  * 1-based numbers of the pages that paint a `/ShadingType 1` shading whose
45
47
  * function is `/FunctionType 4` (a PostScript calculator function).
@@ -56,7 +58,7 @@ export declare function hasLiveTransparency(pdfBytes: Uint8Array): Promise<boole
56
58
  *
57
59
  * Walks page resources (and form XObjects they draw) rather than every
58
60
  * object, so only shadings a page can actually paint are reported. Like
59
- * {@link hasLiveTransparency}, it never fails a build: an unparseable PDF
61
+ * {@link transparentPages}, it never fails a build: an unparseable PDF
60
62
  * reports no pages.
61
63
  */
62
64
  export declare function findPostScriptShadingPages(pdfBytes: Uint8Array): Promise<number[]>;
@@ -81,6 +83,58 @@ export declare function findPostScriptShadingPages(pdfBytes: Uint8Array): Promis
81
83
  * directly (pdf-lib, in-process, no second external tool).
82
84
  */
83
85
  export declare function stripAnnotations(pdfPath: string, stagingDir: string): Promise<void>;
86
+ /** Why a transparent page is flattened whole under `preserve-text` instead of split. */
87
+ export type FallbackCause = Exclude<UnsplittableReason, "no-text"> | "listed" | "gradient";
88
+ /**
89
+ * Sort each page that uses transparency (1-based, from {@link transparentPages})
90
+ * for `pdfx.transparency: preserve-text`:
91
+ * - `split`: its artwork is flattened and its text kept live
92
+ * ({@link flattenTransparentPages});
93
+ * - `artOnly`: it shows no text, so there is nothing to keep and Ghostscript
94
+ * flattens it whole, which is as good;
95
+ * - `kept`: it has text but is flattened whole anyway, with the cause the
96
+ * build reports. The author's `pdfx.flattenPages` list wins, then the
97
+ * text's own reason (`unsplittableTextReasons`), then a repeating or conic
98
+ * gradient (#386), whose PostScript-function shading the artwork pass
99
+ * distorts.
100
+ * Every transparent page lands in exactly one group.
101
+ */
102
+ export declare function planTransparentPages(transparent: number[], reasons: Map<number, UnsplittableReason>, flattenPages: number[], gradientPages: number[]): {
103
+ split: number[];
104
+ artOnly: number[];
105
+ kept: Map<number, FallbackCause>;
106
+ };
107
+ /**
108
+ * Make the pages in `pages` (1-based; the `split` group of
109
+ * {@link planTransparentPages}) free of transparency while keeping
110
+ * their text live, so PDF/X (PDF 1.3) conversion has nothing left to flatten
111
+ * into a single bitmap. Opt-in: `pdfx.transparency: preserve-text`.
112
+ *
113
+ * Per affected page, two Ghostscript passes split it in two and pdf-lib
114
+ * stacks the halves back on the ORIGINAL page object:
115
+ *
116
+ * 1. ARTWORK — `pdfimage24 -dFILTERTEXT -r<dpi>` drops the glyphs and
117
+ * composites everything left (soft shadows, alpha images, opacity) into
118
+ * one opaque image (300 DPI unless `pdfx.rasterDpi` says otherwise),
119
+ * straight from the source;
120
+ * 2. TEXT — `-dFILTERIMAGE -dFILTERVECTOR` keeps only the glyphs. It needs
121
+ * `-dNOTRANSPARENCY`: without it Chromium's transparency groups and
122
+ * `/ca`/`/SMask` ExtGStates survive in the text-only file and the next
123
+ * step flattens the page anyway. That is only faithful for text with no
124
+ * transparency of its own, which is why callers leave other pages out;
125
+ * 3. the page's content is replaced by the artwork with the text drawn on
126
+ * top. Editing the original page keeps its boxes (Media/Crop/Trim/Bleed),
127
+ * annotations, the document's metadata, outlines and labels exactly as
128
+ * they were; only its `/Contents`, `/Resources` and `/Group` change.
129
+ *
130
+ * Pages not listed are never touched. Known limits, both consequences of
131
+ * splitting a page into "picture" and "text": the page's vector chrome
132
+ * (rules, backgrounds, shapes) becomes the picture, and text is always the
133
+ * top layer, so text another element covers shows through.
134
+ */
135
+ export declare function flattenTransparentPages(pdfPath: string, pageNumbers: number[], stagingDir: string,
136
+ /** artwork resolution (`pdfx.rasterDpi`) */
137
+ dpi?: number): Promise<void>;
84
138
  type PdfxFlavor = "x1a" | "x3";
85
139
  /**
86
140
  * Stamp the Creator metadata field on an existing PDF.
@@ -1,5 +1,6 @@
1
1
  import { type LoadedPlugin } from "./renderer";
2
2
  import { type PrintPadding } from "../../engine/shared/synthesis.ts";
3
+ import { type PrintAreaSource } from "../../engine/shared/print-area.ts";
3
4
  /** Reader injected by the host: resolve a project-root-relative file → its text. */
4
5
  export type ReadText = (relPath: string) => Promise<string>;
5
6
  /**
@@ -9,8 +10,8 @@ export type ReadText = (relPath: string) => Promise<string>;
9
10
  * `type`s (`ambiguous_marker_token`, `unrecognized_marker_token`,
10
11
  * `extra_bare_marker_token`, `unknown_marker`, `nested_spread`,
11
12
  * `continue_without_section`, `spread_without_pages`, `spread_eof_close`,
12
- * `page_outside_spread`, `pin_outside_page`, `unknown_gp_class` — the last
13
- * emitted by `gp-pin-scope.js`'s `gp_pin_scope_check`, same as
13
+ * `page_outside_spread`, `pin_outside_page`, `unknown_gp_class`, `flush_art` —
14
+ * the last two emitted by `gp-pin-scope.js`'s `gp_pin_scope_check`, same as
14
15
  * `pin_outside_page`, see #226).
15
16
  *
16
17
  * #240 (declarative container components) adds three more, all emitted by
@@ -126,6 +127,15 @@ export interface AssembleBookHtmlOptions {
126
127
  * engine/shared/synthesis.ts) and the two page counts agree.
127
128
  */
128
129
  print?: PrintPadding;
130
+ /**
131
+ * The project's print area — the resolved manifest's `page` block plus its
132
+ * preset id and printer spec — written as `<meta name="gp-print-area">`
133
+ * (engine/shared/print-area.ts) into EVERY assembled book, preview, build
134
+ * and publish alike: the preview's print overlays judge the ink against it,
135
+ * and both engines place pinned art on its art-safe line. Metadata only:
136
+ * nothing renders from it but what reads it.
137
+ */
138
+ printArea?: PrintAreaSource;
129
139
  plugins?: LoadedPlugin[];
130
140
  pluginCss?: string;
131
141
  /** Add a layout-neutral source-file id to source-mapped preview blocks. */
@@ -31,7 +31,10 @@
31
31
  * .gp-full — fills the page's content width.
32
32
  * .gp-small — width 25% of the column. Sizes compose with any
33
33
  * .gp-medium — width 50%. position (including .gp-pin, where
34
- * .gp-large — width 75%. the % resolves against the page box).
34
+ * .gp-large — width 60%. the % resolves against the page
35
+ * .gp-xl — width 80%. root: the page box, reaching the
36
+ * .gp-xxl — width 90%. paper on an edge a pin uses).
37
+ * There is no 100% step: that is .gp-full, a position.
35
38
  * .gp-tight — float/shape clearance 0.5em (sets --gp-gap; default 1em).
36
39
  * .gp-loose — float/shape clearance 2em (sets --gp-gap).
37
40
  * .gp-shape — wraps text to a floated image's alpha silhouette
@@ -47,16 +50,14 @@
47
50
  * frame instead, BY DESIGN: that is how a pin is scoped to
48
51
  * a card rather than a sheet. Page-level furniture must
49
52
  * therefore be authored outside such a wrapper.
50
- * Centered on both axes unless combined with the edge
51
- * modifiers .gp-top/.gp-bottom/.gp-left/.gp-right.
52
- * .gp-flush — with .gp-pin + an edge, the art sits on the PAPER's
53
- * edge rather than on the text block's. No CSS rule here:
54
- * the class is a marker both ENGINES implement (see
55
- * engine/shared/flush.ts), because reaching the paper
56
- * requires freeing that page's margin — per page — and
57
- * relocating the furniture that lived in it, neither of
58
- * which a stylesheet can do. Inert without .gp-pin + an
59
- * edge word, and inert under plain markdown-it.
53
+ * Centered in the text block unless combined with the
54
+ * edge modifiers .gp-top/.gp-bottom/.gp-left/.gp-right;
55
+ * an edge pin sits on the printer's ART-SAFE line of that
56
+ * edge and never moves the text — see "the pin box" below.
57
+ * .gp-flush — with .gp-pin + an edge, the escape hatch: the art runs
58
+ * to the PAPER's edge, past the printer's safe area. It
59
+ * only zeroes the pin box's insets (the rule after the
60
+ * edge modifiers). Inert without .gp-pin + an edge word.
60
61
  * .gp-bleed — forces its own page (break-before) and spans it
61
62
  * edge-to-edge horizontally. This does NOT cancel the
62
63
  * top/bottom margins, extend past the trim into printer
@@ -102,12 +103,12 @@
102
103
  * paragraph with other inline content keeps its margin.
103
104
  *
104
105
  * Rule ORDER within the gp-* block is the contract: flow positions → sizes
105
- * → spacing → .gp-pin → pin-edge modifiers. Everything is flat 0-1-0
106
- * specificity, so combining classes resolves by source order: a later flow
107
- * position wins (left < right < center < full < bleed); an explicit size's
108
- * max-width:100% lifts the floats' 50% cap; .gp-pin's margin:0 /
109
- * max-width:100% beat the float declarations; the edge modifiers beat
110
- * .gp-pin's center defaults.
106
+ * → spacing → .gp-pin → pin-edge modifiers → .gp-flush. Everything else is
107
+ * flat 0-1-0 specificity, so combining classes resolves by source order: a
108
+ * later flow position wins (left < right < center < full < bleed); an
109
+ * explicit size's max-width:100% lifts the floats' 50% cap; .gp-pin's
110
+ * margin:0 / max-width:100% beat the float declarations; the edge modifiers
111
+ * beat .gp-pin's center defaults.
111
112
  *
112
113
  * .gp-pin semantics, stated honestly: it pins within its @page/@spread
113
114
  * CONTAINER, not "the sheet edge". A .page div that fragments across
@@ -120,12 +121,29 @@
120
121
  * compiler's engine.abspos.leak diagnostic catches the raw-HTML cases the
121
122
  * token walk can't see.
122
123
  *
124
+ * The PIN BOX — where a pin may sit, and why the text never moves. An edge
125
+ * pin has to be able to reach the paper, and Chromium's printable area IS
126
+ * the page area (engine/shared/pin-edges.ts has the measurements), so both
127
+ * engines free the page margin on each edge a pin names: print through a
128
+ * generated named page, the preview through the run's strip geometry. The
129
+ * page root gets that margin back as padding, on top of any of its own
130
+ * (`box-decoration-break: clone`), so its content box — the text — is the
131
+ * author's content box, same size, same place, same breaks, wherever the art
132
+ * is pinned. The pin's containing
133
+ * block is the root's padding box, and the engine sets `--gp-pin-<edge>` on
134
+ * the root to the art-safe line's distance from the sheet edge (the bleed
135
+ * plus page.artSafe on the top, bottom and outside edge; page.safe at the
136
+ * binding, from the manifest); `.gp-pin` reads them as its four insets.
137
+ * `.gp-flush` zeroes them on the pin: the paper's edge. Under plain
138
+ * markdown-it nothing sets them and every inset is 0.
139
+ *
123
140
  * Pin CSS mechanics (all three are load-bearing, verified by
124
- * paged-css-image-pin.test.ts): `inset: 0` is REQUIRED — abspos
125
- * self-alignment aligns within the inset-modified containing block, and
126
- * with auto insets that collapses to the static-position rectangle, where
127
- * alignment does nothing. The center defaults must be EXPLICIT — `normal`
128
- * alignment behaves as `start` for abspos replaced elements, not center.
141
+ * marker-css-image-pin.test.ts): four non-auto insets are REQUIRED — abspos
142
+ * self-alignment aligns within the inset-modified containing block (the pin
143
+ * box), and with auto insets that collapses to the static-position
144
+ * rectangle, where alignment does nothing. The center defaults must be
145
+ * EXPLICIT — `normal` alignment behaves as `start` for abspos replaced
146
+ * elements, not center.
129
147
  * And `justify-self` on .gp-left/.gp-right does double duty (flow float +
130
148
  * pin edge) because self-alignment does not apply to in-flow floats — it is
131
149
  * inert until .gp-pin makes the element abspos. If Chrome ever ships
@@ -133,16 +151,14 @@
133
151
  * carrying gp-left could start shifting; that is standards-tracking per
134
152
  * CLAUDE.md ("Chrome wins once it ships"), not a bug in the author's book.
135
153
  */
136
- export declare const GUTTERPRESS_CSS = "\n/* gp-* author image/block vocabulary. One vocabulary, gp-* only \u2014 the\n pre-vocabulary utility names (.center/.float-left/.float-right/\n .full-width/.full-bleed) were REMOVED when gp-* shipped; books rename\n the classes in their markdown (see the migration note). Source ORDER is\n the contract \u2014 see the doctrine comment above. */\n\n/* flow positions */\n.gp-left {\n float: left;\n margin: 0 var(--gp-gap, 1em) var(--gp-gap, 1em) 0;\n max-width: 50%;\n}\n.gp-right {\n float: right;\n margin: 0 0 var(--gp-gap, 1em) var(--gp-gap, 1em);\n max-width: 50%;\n}\n.gp-center {\n display: block;\n float: none;\n margin-left: auto;\n margin-right: auto;\n max-width: 100%;\n}\n.gp-full {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n@page gp-full-bleed { margin-left: 0; margin-right: 0; }\n.gp-bleed {\n display: block;\n float: none;\n break-before: page;\n page: gp-full-bleed;\n max-width: none;\n width: 100%;\n margin-left: 0;\n margin-right: 0;\n}\n\n/* sizes \u2014 AFTER the flow positions so max-width:100% lifts the floats' 50%\n cap at equal specificity */\n.gp-small { width: 25%; max-width: 100%; }\n.gp-medium { width: 50%; max-width: 100%; }\n.gp-large { width: 75%; max-width: 100%; }\n\n/* float clearance presets \u2014 consumed by var(--gp-gap) in the float rules\n above and by .gp-shape's shape-margin below; --gp-gap itself is\n author-settable CSS */\n.gp-tight { --gp-gap: 0.5em; }\n.gp-loose { --gp-gap: 2em; }\n\n/* column runs \u2014 plain CSS Multi-column, emitted by the core @columns marker\n (markers.js): @columns [1-5] ... @end-columns is the ONE way to lay\n content out in columns, and @column-break moves to the next column.\n The classes below are the CSS that marker emits, not author vocabulary.\n\n Permanent vocabulary, not a shim: Chromium implements multicol natively\n and these rules are the standard properties verbatim, so there is no\n spec gap here to remove later. Deliberately minimal \u2014 column-fill is\n NOT set (the CSS initial balance applies).\n Themes style a run through two tokens instead of writing column rules:\n --gp-column-gap default 1.5em\n --gp-column-rule default none (any column-rule shorthand value)\n Both hooks sit at zero specificity (:where), so any author rule wins.\n .gp-columns is two columns on its own; a count class (bare @columns\n emits gp-columns-2) wins because it comes later at the same specificity. */\n:where(.gp-columns, .gp-columns-1, .gp-columns-2, .gp-columns-3, .gp-columns-4, .gp-columns-5) {\n column-gap: var(--gp-column-gap, 1.5em);\n column-rule: var(--gp-column-rule, none);\n}\n.gp-columns { columns: 2; }\n.gp-columns-1 { columns: 1; }\n.gp-columns-2 { columns: 2; }\n.gp-columns-3 { columns: 3; }\n.gp-columns-4 { columns: 4; }\n.gp-columns-5 { columns: 5; }\n\n/* fragmentation controls \u2014 generic pagination utilities for any element,\n independent of the column vocabulary above (a book reached for\n these just as often outside a multicol run: keeping a card whole,\n forcing a section to start a fresh page). Standard properties only \u2014\n this project is Chromium-only (CLAUDE.md), so there are no legacy\n page-break-* twins to also emit. One name each. */\n.gp-no-break { break-inside: avoid; }\n.gp-break-before { break-before: page; }\n\n/* shape wrap \u2014 text follows the image's alpha silhouette instead of its\n rectangular box. shape-outside only applies to floats, so this is inert\n without .gp-left/.gp-right (and under .gp-pin, which un-floats). The\n shape URL cannot be written in CSS (url() contexts can't read attr()),\n so the image renderer rule (images.ts) mirrors the src into an inline\n --gp-shape:url(...) custom property whenever it sees this class --\n authors only ever type the class. threshold 0.2 ignores near-transparent\n anti-aliasing halos; shape-margin shares the float-gap vocabulary. */\nimg.gp-shape {\n shape-outside: var(--gp-shape);\n shape-image-threshold: 0.2;\n shape-margin: var(--gp-gap, 1em);\n}\n\n/* pin \u2014 within the nearest positioned ancestor (.page/.spread, rule above).\n inset:0 and the explicit centers are load-bearing; see doctrine comment. */\n.gp-pin {\n position: absolute;\n inset: 0;\n align-self: center;\n justify-self: center;\n margin: 0;\n max-width: 100%;\n}\n\n/* pin edge modifiers \u2014 AFTER .gp-pin to beat its center defaults;\n justify-self is inert on in-flow floats, so gp-left/gp-right safely do\n double duty as flow float + pin edge */\n.gp-top { align-self: start; }\n.gp-bottom { align-self: end; }\n.gp-left { justify-self: start; }\n.gp-right { justify-self: end; }\n\n/* wrapper-margin neutralization (same pattern and rationale as the\n .gp-bleed paragraph-margin note in the doctrine comment; for pin, the\n emptied paragraph would otherwise leave a phantom margin gap in flow) */\n:where(p:has(> img.gp-bleed:only-child)) { margin: 0; }\n:where(p:has(> img.gp-pin:only-child)) { margin: 0; }\n\n/* depth \u2014 a named ladder for z-index, so books stop hand-tuning bare\n integers. A real book measured 21 z-index declarations using only four\n distinct values (-1, 0, 1, 2), each written literally at its use site.\n The custom properties are the author-settable surface (a book needing a\n deeper stack raises them once); the classes are the shorthand.\n\n NOT named \"layer\": CSS Paged Media 3 \u00A73.1 already defines \"page layers\"\n (page background, canvas, borders, contents, margin boxes) and those are\n parts of the PAGE BOX, not a z-ladder for content. Reusing the word for a\n different concept would collide with the spec vocabulary this project\n tracks. The pin EDGE modifiers already own .gp-top/.gp-bottom, so the\n ladder avoids those words too.\n\n .gp-behind is the one that earns its place: it puts a pinned image UNDER\n the page's text, which is otherwise impossible to express without a bare\n negative z-index. \"Above\" needs no class \u2014 an out-of-flow pin already\n paints above in-flow content.\n\n Two things silently defeat .gp-behind, neither visible at the use site:\n - a stacking context on the .page/.spread ancestor (z-index, isolation,\n opacity, filter, transform on it traps the negative layer inside).\n Core keeps .page/.spread at 'position: relative; z-index: auto'\n precisely so they are not stacking contexts.\n - a clipping ancestor (overflow other than visible) \u2014 but only where\n the art actually overhangs that ancestor's clip box on a clipped\n axis: the overhang is cut off, the same mechanism that clips a\n .gp-bleed plate back to the wrapper's width. Clipping never reorders\n layers \u2014 within-bounds art under a clipping .page prints whole and\n still behind (measured; see the build audit's comment in\n engine/compiler/build.ts), and a static wrapper's overflow never\n binds an abspos .gp-pin at all.\n The build-time engine.layer.trapped audit reports both against the live\n ancestor chain. printsafe/page-containment is only an early source hint for\n declarations written directly on .page/.spread. */\n:root {\n --gp-z-behind: -1;\n --gp-z-base: 0;\n --gp-z-raised: 1;\n --gp-z-front: 2;\n}\n.gp-behind { z-index: var(--gp-z-behind); }\n.gp-base { z-index: var(--gp-z-base); }\n.gp-raised { z-index: var(--gp-z-raised); }\n.gp-front { z-index: var(--gp-z-front); }\n\n/* GFM-style alert/callout boxes (#237) \u2014 the DOM gfm-alerts.ts emits from\n \"> [!TYPE]\" blockquote syntax when a project opts into the bundled\n gutterpress-gfm-alerts feature (BUILTIN_OPTIONAL_PLUGINS, renderer.ts).\n The plugin is optional and inert by default; these rules are equally\n inert wherever the classes never appear, exactly like every other\n utility above, so shipping them costs nothing to a book that has not\n turned the feature on.\n\n This is authored-COMPONENT vocabulary, not the @marker structural family\n (markers.js/MARKER_CSS) \u2014 CLAUDE.md \u00A76 splits the two core CSS blocks by\n ROLE, and an author-facing box an author invokes via inline syntax is the\n same role as every other gp-* utility here, not page/chapter/section DOM.\n\n Deliberately minimal (CLAUDE.md \"thin over capable\" / the issue's own\n \"a rule and a label\"): a left border plus a bold label, :where()'d to\n zero specificity so a theme's own (unlayered) rule for any of these\n classes wins outright regardless of selector weight \u2014 see the\n cascade-layers note atop this file. Five semantic accent colors (the same\n NOTE/TIP/IMPORTANT/WARNING/CAUTION hues GitHub's own alerts use \u2014 a\n long-established, non-proprietary convention, not DC branding) are\n exposed as :root custom properties, the same author-settable-surface\n pattern as the --gp-z-* ladder above: a book retints every alert of one\n type by overriding a single property, no specificity fight required.\n --gp-alert-color cascades from the type modifier down to .gp-alert-title\n through ordinary CSS inheritance (the title is always a DOM descendant of\n the box), so it is set in exactly five places, not ten. */\n:root {\n --gp-alert-note-color: #0969da;\n --gp-alert-tip-color: #1a7f37;\n --gp-alert-important-color: #8250df;\n --gp-alert-warning-color: #9a6700;\n --gp-alert-caution-color: #cf222e;\n}\n:where(.gp-alert) {\n margin: 1em 0;\n padding: 0.5em 1em;\n border-left: 0.25em solid var(--gp-alert-color, currentColor);\n}\n:where(.gp-alert-title) {\n margin: 0 0 0.5em;\n font-weight: bold;\n color: var(--gp-alert-color, inherit);\n}\n:where(.gp-alert-note) { --gp-alert-color: var(--gp-alert-note-color); }\n:where(.gp-alert-tip) { --gp-alert-color: var(--gp-alert-tip-color); }\n:where(.gp-alert-important) { --gp-alert-color: var(--gp-alert-important-color); }\n:where(.gp-alert-warning) { --gp-alert-color: var(--gp-alert-warning-color); }\n:where(.gp-alert-caution) { --gp-alert-color: var(--gp-alert-caution-color); }\n";
154
+ export declare const GUTTERPRESS_CSS = "\n/* gp-* author image/block vocabulary. One vocabulary, gp-* only \u2014 the\n pre-vocabulary utility names (.center/.float-left/.float-right/\n .full-width/.full-bleed) were REMOVED when gp-* shipped; books rename\n the classes in their markdown (see the migration note). Source ORDER is\n the contract \u2014 see the doctrine comment above. */\n\n/* flow positions */\n.gp-left {\n float: left;\n margin: 0 var(--gp-gap, 1em) var(--gp-gap, 1em) 0;\n max-width: 50%;\n}\n.gp-right {\n float: right;\n margin: 0 0 var(--gp-gap, 1em) var(--gp-gap, 1em);\n max-width: 50%;\n}\n.gp-center {\n display: block;\n float: none;\n margin-left: auto;\n margin-right: auto;\n max-width: 100%;\n}\n.gp-full {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n@page gp-full-bleed { margin-left: 0; margin-right: 0; }\n.gp-bleed {\n display: block;\n float: none;\n break-before: page;\n page: gp-full-bleed;\n max-width: none;\n width: 100%;\n margin-left: 0;\n margin-right: 0;\n}\n\n/* sizes \u2014 AFTER the flow positions so max-width:100% lifts the floats' 50%\n cap at equal specificity */\n.gp-small { width: 25%; max-width: 100%; }\n.gp-medium { width: 50%; max-width: 100%; }\n.gp-large { width: 60%; max-width: 100%; }\n.gp-xl { width: 80%; max-width: 100%; }\n.gp-xxl { width: 90%; max-width: 100%; }\n\n/* float clearance presets \u2014 consumed by var(--gp-gap) in the float rules\n above and by .gp-shape's shape-margin below; --gp-gap itself is\n author-settable CSS */\n.gp-tight { --gp-gap: 0.5em; }\n.gp-loose { --gp-gap: 2em; }\n\n/* column runs \u2014 plain CSS Multi-column, emitted by the core @columns marker\n (markers.js): @columns [1-5] ... @end-columns is the ONE way to lay\n content out in columns, and @column-break moves to the next column.\n The classes below are the CSS that marker emits, not author vocabulary.\n\n Permanent vocabulary, not a shim: Chromium implements multicol natively\n and these rules are the standard properties verbatim, so there is no\n spec gap here to remove later. Deliberately minimal \u2014 column-fill is\n NOT set (the CSS initial balance applies).\n Themes style a run through two tokens instead of writing column rules:\n --gp-column-gap default 1.5em\n --gp-column-rule default none (any column-rule shorthand value)\n Both hooks sit at zero specificity (:where), so any author rule wins.\n .gp-columns is two columns on its own; a count class (bare @columns\n emits gp-columns-2) wins because it comes later at the same specificity. */\n:where(.gp-columns, .gp-columns-1, .gp-columns-2, .gp-columns-3, .gp-columns-4, .gp-columns-5) {\n column-gap: var(--gp-column-gap, 1.5em);\n column-rule: var(--gp-column-rule, none);\n}\n.gp-columns { columns: 2; }\n.gp-columns-1 { columns: 1; }\n.gp-columns-2 { columns: 2; }\n.gp-columns-3 { columns: 3; }\n.gp-columns-4 { columns: 4; }\n.gp-columns-5 { columns: 5; }\n\n/* fragmentation controls \u2014 generic pagination utilities for any element,\n independent of the column vocabulary above (a book reached for\n these just as often outside a multicol run: keeping a card whole,\n forcing a section to start a fresh page). Standard properties only \u2014\n this project is Chromium-only (CLAUDE.md), so there are no legacy\n page-break-* twins to also emit. One name each. */\n.gp-no-break { break-inside: avoid; }\n.gp-break-before { break-before: page; }\n\n/* shape wrap \u2014 text follows the image's alpha silhouette instead of its\n rectangular box. shape-outside only applies to floats, so this is inert\n without .gp-left/.gp-right (and under .gp-pin, which un-floats). The\n shape URL cannot be written in CSS (url() contexts can't read attr()),\n so the image renderer rule (images.ts) mirrors the src into an inline\n --gp-shape:url(...) custom property whenever it sees this class --\n authors only ever type the class. threshold 0.2 ignores near-transparent\n anti-aliasing halos; shape-margin shares the float-gap vocabulary. */\nimg.gp-shape {\n shape-outside: var(--gp-shape);\n shape-image-threshold: 0.2;\n shape-margin: var(--gp-gap, 1em);\n}\n\n/* pin \u2014 within the nearest positioned ancestor (.page/.spread, rule above).\n The four insets are the pin box: 0 (the text block's edge) unless an\n engine set --gp-pin-<edge> on the page root for an edge a pin uses (the\n art-safe line). Non-auto insets and the explicit centers are\n load-bearing; see doctrine comment. */\n.gp-pin {\n position: absolute;\n top: var(--gp-pin-top, 0px);\n right: var(--gp-pin-right, 0px);\n bottom: var(--gp-pin-bottom, 0px);\n left: var(--gp-pin-left, 0px);\n align-self: center;\n justify-self: center;\n margin: 0;\n max-width: 100%;\n}\n\n/* pin edge modifiers \u2014 AFTER .gp-pin to beat its center defaults;\n justify-self is inert on in-flow floats, so gp-left/gp-right safely do\n double duty as flow float + pin edge */\n.gp-top { align-self: start; }\n.gp-bottom { align-self: end; }\n.gp-left { justify-self: start; }\n.gp-right { justify-self: end; }\n\n/* the escape hatch \u2014 the paper's edge, past the printer's safe area: the\n pin box loses its art-safe insets. Set on the pin itself, so it beats the\n values the page root hands down. */\n.gp-pin.gp-flush {\n --gp-pin-top: 0px;\n --gp-pin-right: 0px;\n --gp-pin-bottom: 0px;\n --gp-pin-left: 0px;\n}\n\n/* wrapper-margin neutralization (same pattern and rationale as the\n .gp-bleed paragraph-margin note in the doctrine comment; for pin, the\n emptied paragraph would otherwise leave a phantom margin gap in flow) */\n:where(p:has(> img.gp-bleed:only-child)) { margin: 0; }\n:where(p:has(> img.gp-pin:only-child)) { margin: 0; }\n\n/* depth \u2014 a named ladder for z-index, so books stop hand-tuning bare\n integers. A real book measured 21 z-index declarations using only four\n distinct values (-1, 0, 1, 2), each written literally at its use site.\n The custom properties are the author-settable surface (a book needing a\n deeper stack raises them once); the classes are the shorthand.\n\n NOT named \"layer\": CSS Paged Media 3 \u00A73.1 already defines \"page layers\"\n (page background, canvas, borders, contents, margin boxes) and those are\n parts of the PAGE BOX, not a z-ladder for content. Reusing the word for a\n different concept would collide with the spec vocabulary this project\n tracks. The pin EDGE modifiers already own .gp-top/.gp-bottom, so the\n ladder avoids those words too.\n\n .gp-behind is the one that earns its place: it puts a pinned image UNDER\n the page's text, which is otherwise impossible to express without a bare\n negative z-index. \"Above\" needs no class \u2014 an out-of-flow pin already\n paints above in-flow content.\n\n Two things silently defeat .gp-behind, neither visible at the use site:\n - a stacking context on the .page/.spread ancestor (z-index, isolation,\n opacity, filter, transform on it traps the negative layer inside).\n Core keeps .page/.spread at 'position: relative; z-index: auto'\n precisely so they are not stacking contexts.\n - a clipping ancestor (overflow other than visible) \u2014 but only where\n the art actually overhangs that ancestor's clip box on a clipped\n axis: the overhang is cut off, the same mechanism that clips a\n .gp-bleed plate back to the wrapper's width. Clipping never reorders\n layers \u2014 within-bounds art under a clipping .page prints whole and\n still behind (measured; see the build audit's comment in\n engine/compiler/build.ts), and a static wrapper's overflow never\n binds an abspos .gp-pin at all.\n The build-time engine.layer.trapped audit reports both against the live\n ancestor chain. printsafe/page-containment is only an early source hint for\n declarations written directly on .page/.spread. */\n:root {\n --gp-z-behind: -1;\n --gp-z-base: 0;\n --gp-z-raised: 1;\n --gp-z-front: 2;\n}\n.gp-behind { z-index: var(--gp-z-behind); }\n.gp-base { z-index: var(--gp-z-base); }\n.gp-raised { z-index: var(--gp-z-raised); }\n.gp-front { z-index: var(--gp-z-front); }\n\n/* GFM-style alert/callout boxes (#237) \u2014 the DOM gfm-alerts.ts emits from\n \"> [!TYPE]\" blockquote syntax when a project opts into the bundled\n gutterpress-gfm-alerts feature (BUILTIN_OPTIONAL_PLUGINS, renderer.ts).\n The plugin is optional and inert by default; these rules are equally\n inert wherever the classes never appear, exactly like every other\n utility above, so shipping them costs nothing to a book that has not\n turned the feature on.\n\n This is authored-COMPONENT vocabulary, not the @marker structural family\n (markers.js/MARKER_CSS) \u2014 CLAUDE.md \u00A76 splits the two core CSS blocks by\n ROLE, and an author-facing box an author invokes via inline syntax is the\n same role as every other gp-* utility here, not page/chapter/section DOM.\n\n Deliberately minimal (CLAUDE.md \"thin over capable\" / the issue's own\n \"a rule and a label\"): a left border plus a bold label, :where()'d to\n zero specificity so a theme's own (unlayered) rule for any of these\n classes wins outright regardless of selector weight \u2014 see the\n cascade-layers note atop this file. Five semantic accent colors (the same\n NOTE/TIP/IMPORTANT/WARNING/CAUTION hues GitHub's own alerts use \u2014 a\n long-established, non-proprietary convention, not DC branding) are\n exposed as :root custom properties, the same author-settable-surface\n pattern as the --gp-z-* ladder above: a book retints every alert of one\n type by overriding a single property, no specificity fight required.\n --gp-alert-color cascades from the type modifier down to .gp-alert-title\n through ordinary CSS inheritance (the title is always a DOM descendant of\n the box), so it is set in exactly five places, not ten. */\n:root {\n --gp-alert-note-color: #0969da;\n --gp-alert-tip-color: #1a7f37;\n --gp-alert-important-color: #8250df;\n --gp-alert-warning-color: #9a6700;\n --gp-alert-caution-color: #cf222e;\n}\n:where(.gp-alert) {\n margin: 1em 0;\n padding: 0.5em 1em;\n border-left: 0.25em solid var(--gp-alert-color, currentColor);\n}\n:where(.gp-alert-title) {\n margin: 0 0 0.5em;\n font-weight: bold;\n color: var(--gp-alert-color, inherit);\n}\n:where(.gp-alert-note) { --gp-alert-color: var(--gp-alert-note-color); }\n:where(.gp-alert-tip) { --gp-alert-color: var(--gp-alert-tip-color); }\n:where(.gp-alert-important) { --gp-alert-color: var(--gp-alert-important-color); }\n:where(.gp-alert-warning) { --gp-alert-color: var(--gp-alert-warning-color); }\n:where(.gp-alert-caution) { --gp-alert-color: var(--gp-alert-caution-color); }\n";
137
155
  /**
138
156
  * GP_CLASSES — every class an author may legitimately write with a `gp-`
139
157
  * prefix: the `.gp-*` selectors in `GUTTERPRESS_CSS` above, the two
140
158
  * structural classes `markers.js`'s `MARKER_CSS` styles (`gp-page-break`,
141
- * `gp-column-break`), and two classes the marker plugin EMITS but that carry
142
- * no CSS rule of their own — `gp-continued` (a `@continue`d section's marker,
143
- * for author/theme styling — see markers.js's header) and `gp-flush` (a
144
- * `.gp-pin` edge modifier implemented in the engine's layout code, not CSS —
145
- * see the `.gp-flush` doctrine note above).
159
+ * `gp-column-break`), and one class the marker plugin EMITS but that carries
160
+ * no CSS rule of its own — `gp-continued` (a `@continue`d section's marker,
161
+ * for author/theme styling — see markers.js's header).
146
162
  *
147
163
  * This is the vocabulary `gp-pin-scope.js`'s `unknown_gp_class` diagnostic
148
164
  * (#226) checks every author-facing class against: any `gp-`-prefixed class
@@ -151,8 +167,8 @@ export declare const GUTTERPRESS_CSS = "\n/* gp-* author image/block vocabulary.
151
167
  * classes are none of core's business.
152
168
  *
153
169
  * `gutterpress-css.test.ts` asserts this set and the `.gp-*` selectors
154
- * textually present in `GUTTERPRESS_CSS` + `MARKER_CSS` agree (modulo the two
155
- * marker-only exceptions above), so the two cannot silently drift apart —
170
+ * textually present in `GUTTERPRESS_CSS` + `MARKER_CSS` agree (modulo the
171
+ * marker-only exception above), so the two cannot silently drift apart —
156
172
  * whoever adds a class to one CSS block and forgets this list finds out from
157
173
  * a failing test, not from a future bug report.
158
174
  */
@@ -2,6 +2,7 @@ import { type LayoutWarning } from "./assemble";
2
2
  import { type AssetCopy } from "../asset-inline";
3
3
  import type { LoadedPlugin, PluginStyleGroup } from "./renderer";
4
4
  import type { PrintPadding } from "../../engine/shared/synthesis.ts";
5
+ import type { PrintAreaSource } from "../../engine/shared/print-area.ts";
5
6
  export type { LayoutWarning } from "./assemble";
6
7
  /**
7
8
  * THE canonical "which markdown files make up this book, and in what order?"
@@ -33,6 +34,8 @@ export declare function renderChapters(inputDir: string, opts?: {
33
34
  title?: string;
34
35
  /** See {@link AssembleBookHtmlOptions.print}. */
35
36
  print?: PrintPadding;
37
+ /** See {@link AssembleBookHtmlOptions.printArea}. */
38
+ printArea?: PrintAreaSource;
36
39
  styles?: string[];
37
40
  files?: string[] | null;
38
41
  plugins?: LoadedPlugin[];
@@ -72,6 +75,8 @@ export declare function renderChaptersToFile(inputDir: string, outDir: string, o
72
75
  title?: string;
73
76
  /** See {@link AssembleBookHtmlOptions.print}. */
74
77
  print?: PrintPadding;
78
+ /** See {@link AssembleBookHtmlOptions.printArea}. */
79
+ printArea?: PrintAreaSource;
75
80
  styles?: string[];
76
81
  files?: string[] | null;
77
82
  plugins?: LoadedPlugin[];
@@ -134,6 +134,51 @@ export interface OpPass {
134
134
  fonts: FontRef[];
135
135
  }
136
136
  export declare function getOpPass(doc: PDFDocumentProxy): Promise<OpPass>;
137
+ /**
138
+ * Why a page is not split from its artwork: its text cannot be (the first
139
+ * three), or it shows no text at all, so there is none to keep (`no-text`).
140
+ */
141
+ export type UnsplittableReason = "text-transparency" | "type3-font" | "text-pattern-or-clip" | "no-text";
142
+ /**
143
+ * For each 1-based page that `pdfx.transparency: preserve-text` must not split,
144
+ * the first reason found; pages absent from the map can be split. `pages`
145
+ * limits the scan to those pages (the build passes the transparent ones only,
146
+ * so the rest of the book is never decoded).
147
+ *
148
+ * A page's text cannot be split when text SHOWS while transparency is in effect: constant alpha
149
+ * (`ca`/`CA` < 1), a non-Normal blend mode, an active soft mask, or any
150
+ * enclosing transparency group that is composited translucent, blended or
151
+ * masked, or is a knockout group. (Chromium draws `opacity` and
152
+ * `mix-blend-mode` content as a group form XObject under such a state, and
153
+ * resets the state INSIDE the group, so the state at the point the group
154
+ * begins is what counts.) Such text cannot be lifted out of its page and drawn
155
+ * opaque on top of a flattened picture without changing how it looks, so
156
+ * `pdfx.transparency: preserve-text` leaves those pages alone.
157
+ *
158
+ * Also flagged: any page that sets a Type 3 font. Chromium (Skia) emits Type 3
159
+ * for glyphs it can only draw as paths, e.g. the synthetic bold of a
160
+ * single-weight webfont. Their glyph procedures are vector paths, which the
161
+ * text pass's `-dFILTERVECTOR` strips, so the text would print nothing.
162
+ *
163
+ * Also flagged: text painted with a pattern (`scn`/`SCN` to a tiling pattern
164
+ * or shading), as Chromium emits for SVG `<text fill="url(#…)">` and gradient
165
+ * text fills, and text used as a clip (render modes 4-7, `background-clip:
166
+ * text`). The artwork pass removes the glyphs and the text pass's
167
+ * `-dFILTERIMAGE`/`-dFILTERVECTOR` removes their paint, so they would vanish.
168
+ *
169
+ * A plain isolated group composited at alpha 1 / Normal / no mask is NOT
170
+ * counted: painting it is the same as painting its content inline, and
171
+ * Chromium wraps the content of EVERY page in one as soon as any element of
172
+ * the document uses a blend mode (measured on Chromium 141), which would
173
+ * otherwise rule out every page of such a book. The page's OWN `/Group` is
174
+ * likewise ignored.
175
+ *
176
+ * Walks PDF.js's operator list, which has already inlined form XObjects
177
+ * between `paintFormXObjectBegin/End` and reports their `/Group` as
178
+ * `beginGroup/endGroup`. Graphics state is tracked per save/restore (a form
179
+ * XObject is an implicit save/restore).
180
+ */
181
+ export declare function unsplittableTextReasons(pdfBytes: Uint8Array, pages?: number[]): Promise<Map<number, UnsplittableReason>>;
137
182
  /**
138
183
  * Does page `pageNum` (1-based) paint anything at all? False for a page with
139
184
  * no content and for one whose operators only set state or clip. A path op
@@ -155,9 +155,21 @@
155
155
  "enum": ["icc", "k-only"],
156
156
  "default": "icc"
157
157
  },
158
+ "transparency": {
159
+ "type": "string",
160
+ "description": "What a PDF/X build does with a page that uses transparency (an alpha-channel image, a soft `rgba()` shadow, CSS `opacity` or `mix-blend-mode`), which PDF/X (PDF 1.3) cannot represent. `flatten-page` leaves the page to Ghostscript, which rasterizes the WHOLE page: it looks right but loses the page's text and fonts. `preserve-text` flattens only the artwork to a picture (300 DPI, or `rasterDpi`) and draws the text live on top, so the page stays searchable. Text is then always the top layer (text another element covers shows through). A page whose text is translucent or blended, set in a Type 3 font (e.g. a synthetic bold or a text stroke), filled with a pattern or used as a clip, or that has a repeating or conic gradient, falls back to `flatten-page`, and so does any page listed in `flattenPages`. Set `validate.assets.allowAlpha: true` to use alpha-channel images. Same for every preset: `flatten-page`.",
161
+ "enum": ["flatten-page", "preserve-text"],
162
+ "default": "flatten-page"
163
+ },
164
+ "flattenPages": {
165
+ "type": "array",
166
+ "description": "With `transparency: preserve-text`: pages (1-based, the numbers the build reports) left to Ghostscript's whole-page flattening even if they could be split, e.g. a page whose look depends on a cover or tint painted over its text. Page numbers move when the book reflows. Default: none.",
167
+ "items": { "type": "integer", "minimum": 1 },
168
+ "examples": [[4, 13]]
169
+ },
158
170
  "rasterDpi": {
159
171
  "type": "integer",
160
- "description": "Resolution (DPI) of the picture Ghostscript makes of a page it flattens in a PDF/X build because the page uses transparency (PDF/X-1a and X-3 cannot hold it). Lower is smaller and softer; vector content on other pages and the book's own images are not resampled. Unset: Ghostscript's default, 720.",
172
+ "description": "Resolution (DPI) of every picture made from a page that uses transparency: the whole pages Ghostscript flattens and the `preserve-text` artwork. Vector content and the book's own images are not resampled. Unset: Ghostscript's default (720) for whole pages, 300 for `preserve-text` artwork.",
161
173
  "minimum": 72,
162
174
  "maximum": 1200,
163
175
  "examples": [300]
@@ -210,19 +222,19 @@
210
222
  },
211
223
  "bleed": {
212
224
  "type": "number",
213
- "description": "A bleed, in points, that the page width x height ALREADY CONTAINS on the top, bottom and OUTER edges (none on the binding edge) — how DriveThruRPG's interior file is cut (8.625 x 11.25in for an 8.5 x 11in book). It tells the preview's safe area & bleed overlay and the `pdf.print.safe-margins` check where the trim is, and changes nothing in the PDF. Preset default: dtrpg 9 (0.125in); book and custom none.",
225
+ "description": "A bleed, in points, that the page width x height ALREADY CONTAINS on the top, bottom and OUTER edges (none on the binding edge) — how DriveThruRPG's interior file is cut (8.625 x 11.25in for an 8.5 x 11in book). It tells the preview's safe area & bleed overlay, the `pdf.print.safe-margins` check and pinned art where the trim is, and by itself changes nothing in the PDF. Preset default: dtrpg 9 (0.125in); book and custom none.",
214
226
  "minimum": 0,
215
227
  "examples": [9]
216
228
  },
217
229
  "safe": {
218
230
  "type": "number",
219
- "description": "The TEXT rule, in points: how far inside the trim glyphs must stay, on every edge. For DriveThruRPG it is the \"text at least 0.5in inside the trim\" rule. The preview's overlay outlines it (dashed) and flags text outside it, and the `pdf.print.safe-margins` check fails a PDF whose text is closer. It changes nothing in the PDF, and no book CSS moves it. Preset default: dtrpg 36 (0.5in), book 18 (0.25in); custom none.",
231
+ "description": "The TEXT rule, in points: how far inside the trim glyphs must stay, on every edge. For DriveThruRPG it is the \"text at least 0.5in inside the trim\" rule. The preview's overlay outlines it (dashed) and flags text outside it, and the `pdf.print.safe-margins` check fails a PDF whose text is closer. An image pinned to the binding edge (`.gp-pin`) sits on it, and to any edge where `artSafe` is not set; no book CSS moves it. Preset default: dtrpg 36 (0.5in), book 18 (0.25in); custom none.",
220
232
  "minimum": 0,
221
233
  "examples": [18, 36]
222
234
  },
223
235
  "artSafe": {
224
236
  "type": "number",
225
- "description": "The ART rule, in points: how far inside the trim art that does not bleed (images, backgrounds, borders, rules) must stay on the top, bottom and OUTER edge; at the binding edge art uses `safe`. For DriveThruRPG it is the \"non-bleeding art at least 0.25in from the outside edges (0.5in at the binding)\" rule. Art that runs out to the page's edge is a bleed and is not judged on that edge. The preview's overlay outlines it (dotted) and flags art outside it. Preview overlay only: it changes nothing in the PDF. Absent = the same as `safe`. Preset default: dtrpg 18 (0.25in); book and custom none.",
237
+ "description": "The ART rule, in points: how far inside the trim art that does not bleed (images, backgrounds, borders, rules) must stay on the top, bottom and OUTER edge; at the binding edge art uses `safe`. For DriveThruRPG it is the \"non-bleeding art at least 0.25in from the outside edges (0.5in at the binding)\" rule. Art that runs out to the page's edge is a bleed and is not judged on that edge. The preview's overlay outlines it (dotted) and flags art outside it, and an image pinned to the top, bottom or outer edge (`.gp-pin`) sits on it unless it carries `.gp-flush`. Absent = the same as `safe`. Preset default: dtrpg 18 (0.25in); book and custom none.",
226
238
  "minimum": 0,
227
239
  "examples": [18]
228
240
  }
@@ -7,20 +7,21 @@ import {
7
7
  TARGET_IDS,
8
8
  scaffoldExtension,
9
9
  scaffoldProject
10
- } from "./cli-zacevwcy.js";
10
+ } from "./cli-0ey902sx.js";
11
11
  import {
12
12
  resolveGhostscript
13
- } from "./cli-gh0ym80r.js";
13
+ } from "./cli-h4tfmpg3.js";
14
14
  import {
15
15
  UsageError,
16
16
  rejectExtraPositionals,
17
17
  rejectUnknownFlags
18
- } from "./cli-7z29z4wz.js";
18
+ } from "./cli-fmksazej.js";
19
19
  import"./cli-ht1bcqdd.js";
20
- import"./cli-j802s31j.js";
20
+ import"./cli-61vd4ews.js";
21
21
  import {
22
22
  EXIT_CODES
23
23
  } from "./cli-46ycxe6r.js";
24
+ import"./cli-sth219v1.js";
24
25
  import"./cli-eyyggect.js";
25
26
  import {
26
27
  isToolAvailable
@@ -0,0 +1,35 @@
1
+ import {
2
+ DOC_CACHE_MAX,
3
+ clearPdfCache,
4
+ countLinkAnnotations,
5
+ getImageResolutions,
6
+ getOpPass,
7
+ getOutlineCount,
8
+ getPageLabels,
9
+ getPageSize,
10
+ getTextPass,
11
+ isLoadable,
12
+ loadPdf,
13
+ pageHasMarks,
14
+ pendingGraceDestroyCount,
15
+ retainPdfCache,
16
+ unsplittableTextReasons
17
+ } from "./cli-sth219v1.js";
18
+ import"./cli-37x76zdn.js";
19
+ export {
20
+ unsplittableTextReasons,
21
+ retainPdfCache,
22
+ pendingGraceDestroyCount,
23
+ pageHasMarks,
24
+ loadPdf,
25
+ isLoadable,
26
+ getTextPass,
27
+ getPageSize,
28
+ getPageLabels,
29
+ getOutlineCount,
30
+ getOpPass,
31
+ getImageResolutions,
32
+ countLinkAnnotations,
33
+ clearPdfCache,
34
+ DOC_CACHE_MAX
35
+ };
@@ -2,21 +2,22 @@ import {
2
2
  executeValidation,
3
3
  publishTargetFor,
4
4
  reportMissingTools
5
- } from "./cli-zacevwcy.js";
5
+ } from "./cli-0ey902sx.js";
6
6
  import {
7
7
  log
8
- } from "./cli-gh0ym80r.js";
8
+ } from "./cli-h4tfmpg3.js";
9
9
  import {
10
10
  UsageError,
11
11
  rejectExtraPositionals,
12
12
  rejectUnknownFlags
13
- } from "./cli-7z29z4wz.js";
13
+ } from "./cli-fmksazej.js";
14
14
  import"./cli-ht1bcqdd.js";
15
- import"./cli-j802s31j.js";
15
+ import"./cli-61vd4ews.js";
16
16
  import {
17
17
  BuildError,
18
18
  EXIT_CODES
19
19
  } from "./cli-46ycxe6r.js";
20
+ import"./cli-sth219v1.js";
20
21
  import"./cli-eyyggect.js";
21
22
  import"./cli-asap4t1z.js";
22
23
  import"./cli-5qr3z0qa.js";