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
@@ -9,7 +9,20 @@ import {
9
9
  getAssetsDir,
10
10
  lastPageMode,
11
11
  parseSize
12
- } from "./index-3nr5rzzp.js";
12
+ } from "./index-gm5xbx5w.js";
13
+ import {
14
+ countLinkAnnotations,
15
+ getImageResolutions,
16
+ getOpPass,
17
+ getOutlineCount,
18
+ getPageLabels,
19
+ getPageSize,
20
+ getTextPass,
21
+ isLoadable,
22
+ loadPdf,
23
+ pageHasMarks,
24
+ retainPdfCache
25
+ } from "./index-sth219v1.js";
13
26
  import {
14
27
  INSTALL_HINTS,
15
28
  enhancedPath,
@@ -157,6 +170,8 @@ var DTRPG_PRESET = {
157
170
  icc: "profiles/CGATS21_CRPC1.icc",
158
171
  stripAnnotations: true,
159
172
  blackText: "k-only",
173
+ transparency: "flatten-page",
174
+ flattenPages: [],
160
175
  rasterDpi: undefined
161
176
  },
162
177
  page: {
@@ -242,6 +257,8 @@ var BOOK_PRESET = {
242
257
  icc: "profiles/CGATS21_CRPC1.icc",
243
258
  stripAnnotations: true,
244
259
  blackText: "icc",
260
+ transparency: "flatten-page",
261
+ flattenPages: [],
245
262
  rasterDpi: undefined
246
263
  },
247
264
  page: {
@@ -647,6 +664,14 @@ function resolveWithPreset(cliOverrides, manifest, preset) {
647
664
  if (lastPage !== undefined && lastPage !== "blank" && lastPage !== "short-by-one") {
648
665
  throw new UsageError(`Manifest field \`print.lastPage\` must be "blank" or "short-by-one", got ${JSON.stringify(lastPage)}.`);
649
666
  }
667
+ const transparency = c.pdfx?.transparency ?? m.pdfx?.transparency;
668
+ if (transparency !== undefined && transparency !== "flatten-page" && transparency !== "preserve-text") {
669
+ throw new UsageError(`Manifest field \`pdfx.transparency\` must be "flatten-page" or "preserve-text", got ${JSON.stringify(transparency)}.`);
670
+ }
671
+ const flattenPages = c.pdfx?.flattenPages ?? m.pdfx?.flattenPages;
672
+ if (flattenPages !== undefined && !(Array.isArray(flattenPages) && flattenPages.every((n) => Number.isInteger(n) && n >= 1))) {
673
+ throw new UsageError(`Manifest field \`pdfx.flattenPages\` must be a list of page numbers (1, 2, …), got ${JSON.stringify(flattenPages)}.`);
674
+ }
650
675
  const rasterDpi = c.pdfx?.rasterDpi ?? m.pdfx?.rasterDpi;
651
676
  if (rasterDpi !== undefined && !(Number.isInteger(rasterDpi) && rasterDpi >= 72 && rasterDpi <= 1200)) {
652
677
  throw new UsageError(`Manifest field \`pdfx.rasterDpi\` must be a whole number from 72 to 1200, got ${JSON.stringify(rasterDpi)}.`);
@@ -1964,7 +1989,9 @@ var GUTTERPRESS_CSS = `
1964
1989
  cap at equal specificity */
1965
1990
  .gp-small { width: 25%; max-width: 100%; }
1966
1991
  .gp-medium { width: 50%; max-width: 100%; }
1967
- .gp-large { width: 75%; max-width: 100%; }
1992
+ .gp-large { width: 60%; max-width: 100%; }
1993
+ .gp-xl { width: 80%; max-width: 100%; }
1994
+ .gp-xxl { width: 90%; max-width: 100%; }
1968
1995
 
1969
1996
  /* float clearance presets — consumed by var(--gp-gap) in the float rules
1970
1997
  above and by .gp-shape's shape-margin below; --gp-gap itself is
@@ -2022,10 +2049,16 @@ img.gp-shape {
2022
2049
  }
2023
2050
 
2024
2051
  /* pin — within the nearest positioned ancestor (.page/.spread, rule above).
2025
- inset:0 and the explicit centers are load-bearing; see doctrine comment. */
2052
+ The four insets are the pin box: 0 (the text block's edge) unless an
2053
+ engine set --gp-pin-<edge> on the page root for an edge a pin uses (the
2054
+ art-safe line). Non-auto insets and the explicit centers are
2055
+ load-bearing; see doctrine comment. */
2026
2056
  .gp-pin {
2027
2057
  position: absolute;
2028
- inset: 0;
2058
+ top: var(--gp-pin-top, 0px);
2059
+ right: var(--gp-pin-right, 0px);
2060
+ bottom: var(--gp-pin-bottom, 0px);
2061
+ left: var(--gp-pin-left, 0px);
2029
2062
  align-self: center;
2030
2063
  justify-self: center;
2031
2064
  margin: 0;
@@ -2040,6 +2073,16 @@ img.gp-shape {
2040
2073
  .gp-left { justify-self: start; }
2041
2074
  .gp-right { justify-self: end; }
2042
2075
 
2076
+ /* the escape hatch — the paper's edge, past the printer's safe area: the
2077
+ pin box loses its art-safe insets. Set on the pin itself, so it beats the
2078
+ values the page root hands down. */
2079
+ .gp-pin.gp-flush {
2080
+ --gp-pin-top: 0px;
2081
+ --gp-pin-right: 0px;
2082
+ --gp-pin-bottom: 0px;
2083
+ --gp-pin-left: 0px;
2084
+ }
2085
+
2043
2086
  /* wrapper-margin neutralization (same pattern and rationale as the
2044
2087
  .gp-bleed paragraph-margin note in the doctrine comment; for pin, the
2045
2088
  emptied paragraph would otherwise leave a phantom margin gap in flow) */
@@ -2149,6 +2192,8 @@ var GP_CLASSES = new Set([
2149
2192
  "gp-small",
2150
2193
  "gp-medium",
2151
2194
  "gp-large",
2195
+ "gp-xl",
2196
+ "gp-xxl",
2152
2197
  "gp-tight",
2153
2198
  "gp-loose",
2154
2199
  "gp-columns",
@@ -2165,6 +2210,7 @@ var GP_CLASSES = new Set([
2165
2210
  "gp-pin",
2166
2211
  "gp-top",
2167
2212
  "gp-bottom",
2213
+ "gp-flush",
2168
2214
  "gp-behind",
2169
2215
  "gp-base",
2170
2216
  "gp-raised",
@@ -2178,8 +2224,7 @@ var GP_CLASSES = new Set([
2178
2224
  "gp-alert-caution",
2179
2225
  "gp-page-break",
2180
2226
  "gp-column-break",
2181
- "gp-continued",
2182
- "gp-flush"
2227
+ "gp-continued"
2183
2228
  ]);
2184
2229
 
2185
2230
  // src/lib/markdown/table-align.ts
@@ -2306,6 +2351,21 @@ function hasPinClass(token) {
2306
2351
  const cls = token.attrGet && token.attrGet("class");
2307
2352
  return typeof cls === "string" && cls.split(/\s+/).includes("gp-pin");
2308
2353
  }
2354
+ var PIN_EDGES = ["gp-top", "gp-right", "gp-bottom", "gp-left"];
2355
+ var FLUSH_ART_MSG = "`.gp-flush` puts this image on the paper's edge, past the printer's safe area. Printers trim up to 0.125in there, so keep it for art that is meant to bleed off the page.";
2356
+ function checkFlushArt(token, env, line) {
2357
+ if (token.type !== "image")
2358
+ return;
2359
+ const cls = token.attrGet && token.attrGet("class");
2360
+ if (typeof cls !== "string")
2361
+ return;
2362
+ const classes = cls.split(/\s+/);
2363
+ if (!classes.includes("gp-pin") || !classes.includes("gp-flush"))
2364
+ return;
2365
+ if (!classes.some((c) => PIN_EDGES.includes(c)))
2366
+ return;
2367
+ warn3(env, line, "flush_art", FLUSH_ART_MSG);
2368
+ }
2309
2369
  function elementNameFor(token) {
2310
2370
  switch (token.type) {
2311
2371
  case "layout_chapter_open":
@@ -2365,6 +2425,7 @@ function gpPinScope(md) {
2365
2425
  if (token.type === "inline" && token.children) {
2366
2426
  for (const child of token.children) {
2367
2427
  checkUnknownGpClasses(child, state.env, line);
2428
+ checkFlushArt(child, state.env, line);
2368
2429
  }
2369
2430
  }
2370
2431
  }
@@ -3116,6 +3177,20 @@ function layerSlug(name) {
3116
3177
  return /^[a-z]/.test(slug) ? slug : `x${slug}`;
3117
3178
  }
3118
3179
 
3180
+ // src/engine/shared/print-area.ts
3181
+ function printAreaMeta(page) {
3182
+ const area = JSON.stringify({
3183
+ width: page.width,
3184
+ height: page.height,
3185
+ bleed: page.bleed ?? 0,
3186
+ safe: page.safe ?? 0,
3187
+ artSafe: page.artSafe,
3188
+ preset: page.preset,
3189
+ spec: page.spec
3190
+ });
3191
+ return `<meta name="gp-print-area" content='${area.replace(/&/g, "&amp;").replace(/'/g, "&#39;")}'>`;
3192
+ }
3193
+
3119
3194
  // src/lib/markdown/assemble.ts
3120
3195
  async function assembleBookHtml(opts) {
3121
3196
  const title = opts.title ?? "Document";
@@ -3181,7 +3256,8 @@ ${projectCss.trim()}` : null
3181
3256
  <html lang="en"${printAttrs}>
3182
3257
  <head>
3183
3258
  <meta charset="UTF-8">
3184
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
3259
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">${opts.printArea ? `
3260
+ ${printAreaMeta(opts.printArea)}` : ""}
3185
3261
  <title>${title}</title>${preloadTags}
3186
3262
  <style data-project-css>
3187
3263
  ${inlineCss}
@@ -3638,6 +3714,7 @@ ${css.trim()}
3638
3714
  preloadImages: cssAssetCopies.map((c) => c.to),
3639
3715
  title: opts.title,
3640
3716
  print: opts.print,
3717
+ printArea: opts.printArea,
3641
3718
  plugins: opts.plugins,
3642
3719
  pluginCss,
3643
3720
  annotateSourceChapters: opts.annotateSourceChapters,
@@ -3650,6 +3727,7 @@ async function renderChaptersToFile(inputDir, outDir, opts = {}) {
3650
3727
  const html = await renderChapters(inputDir, {
3651
3728
  title: opts.title,
3652
3729
  print: opts.print,
3730
+ printArea: opts.printArea,
3653
3731
  styles: opts.styles,
3654
3732
  files: opts.files,
3655
3733
  plugins: opts.plugins,
@@ -3731,6 +3809,118 @@ function resolveDeclaredStyles(rawStyles, baseDir, subject) {
3731
3809
  // src/lib/extension-manifest.ts
3732
3810
  import { readFile as readFile4 } from "node:fs/promises";
3733
3811
  import path3 from "node:path";
3812
+ import { satisfies, validRange } from "semver";
3813
+ // package.json
3814
+ var package_default = {
3815
+ name: "gutterpress",
3816
+ version: "0.11.20-alpha.4",
3817
+ description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
3818
+ author: "itlackey",
3819
+ license: "MPL-2.0",
3820
+ keywords: [
3821
+ "markdown",
3822
+ "pdf",
3823
+ "print",
3824
+ "ghostscript",
3825
+ "book",
3826
+ "publishing",
3827
+ "zine"
3828
+ ],
3829
+ repository: {
3830
+ type: "git",
3831
+ url: "https://github.com/dimm-city/gutterpress.git"
3832
+ },
3833
+ homepage: "https://github.com/dimm-city/gutterpress#readme",
3834
+ bugs: {
3835
+ url: "https://github.com/dimm-city/gutterpress/issues"
3836
+ },
3837
+ type: "module",
3838
+ main: "./dist/index.js",
3839
+ types: "./dist/index.d.ts",
3840
+ bin: {
3841
+ gutterpress: "./dist/cli.js"
3842
+ },
3843
+ exports: {
3844
+ ".": {
3845
+ types: "./dist/index.d.ts",
3846
+ default: "./dist/index.js"
3847
+ },
3848
+ "./api": {
3849
+ types: "./dist/api/index.d.ts",
3850
+ default: "./dist/api/index.js"
3851
+ },
3852
+ "./render": {
3853
+ types: "./dist/render.d.ts",
3854
+ default: "./dist/render.js"
3855
+ }
3856
+ },
3857
+ files: [
3858
+ "dist/",
3859
+ "README.md",
3860
+ "LICENSE"
3861
+ ],
3862
+ scripts: {
3863
+ "build:library": "bun scripts/build-engine-bundles.mjs && rimraf dist && bun build src/index.ts src/api/index.ts --target=node --packages=external --splitting --outdir=dist && bun build src/render.ts --target=node --packages=external --outdir=dist && node scripts/check-render-pure.mjs && tsc -p tsconfig.build.json",
3864
+ build: "bun run build:library && bun build src/cli.ts --target=node --packages=external --splitting --outdir=dist",
3865
+ prepublishOnly: "bun run build",
3866
+ test: "bun test",
3867
+ typecheck: "tsc --noEmit",
3868
+ "typecheck:engine-browser": "tsc --noEmit -p src/engine/browser.tsconfig.json",
3869
+ "parity:gate": "bun scripts/native-parity-gate.ts"
3870
+ },
3871
+ engines: {
3872
+ bun: ">=1.0.0",
3873
+ node: ">=22.0.0"
3874
+ },
3875
+ dependencies: {
3876
+ chokidar: "^5.0.0",
3877
+ citty: "^0.2.0",
3878
+ diff3: "0.0.3",
3879
+ fflate: "0.8.3",
3880
+ glob: "^13.0.0",
3881
+ htmlhint: "^1.9.2",
3882
+ ignore: "^5.3.2",
3883
+ "isomorphic-git": "^1.38.4",
3884
+ "markdown-it": "^14.3.2",
3885
+ "markdown-it-abbr": "^2.0.0",
3886
+ "markdown-it-attrs": "^4.3.0",
3887
+ "markdown-it-deflist": "^3.0.1",
3888
+ "markdown-it-footnote": "^4.0.0",
3889
+ "markdown-it-mark": "^4.0.0",
3890
+ "markdown-it-source-map": "^0.1.1",
3891
+ "markdown-it-sub": "^2.0.0",
3892
+ "markdown-it-sup": "^2.0.0",
3893
+ markdownlint: "^0.40.0",
3894
+ "pdf-lib": "^1.17.1",
3895
+ postcss: "^8.5.18",
3896
+ "resolve.exports": "2.0.3",
3897
+ semver: "7.8.0",
3898
+ tar: "7.5.22",
3899
+ unpdf: "^1.6.2",
3900
+ ws: "^8.21.0",
3901
+ yaml: "^2.8.2"
3902
+ },
3903
+ devDependencies: {
3904
+ "@types/bun": "1.3.14",
3905
+ "@types/markdown-it": "^14.1.2",
3906
+ "@types/node": "^22.0.0",
3907
+ "@types/semver": "^7.7.1",
3908
+ "@types/ws": "^8.5.13",
3909
+ playwright: "^1.60.0",
3910
+ rimraf: "^6.0.0",
3911
+ typescript: "^5"
3912
+ }
3913
+ };
3914
+
3915
+ // src/lib/version.ts
3916
+ var PACKAGE_META = {
3917
+ version: package_default.version ?? "unknown",
3918
+ dependencies: package_default.dependencies ?? {},
3919
+ devDependencies: package_default.devDependencies ?? {}
3920
+ };
3921
+ var PACKAGE_VERSION = PACKAGE_META.version;
3922
+
3923
+ // src/lib/extension-manifest.ts
3734
3924
  var EXTENSION_MANIFEST_FILENAME = "package.json";
3735
3925
  var GUTTERPRESS_KEY = "gutterpress";
3736
3926
  function object(value) {
@@ -3754,6 +3944,7 @@ function metaFromPackageJson(raw) {
3754
3944
  ...str(pkg.description) ? { description: str(pkg.description) } : {},
3755
3945
  ...keywords ? { keywords } : {},
3756
3946
  ...str(pkg.main) ? { main: str(pkg.main) } : {},
3947
+ ...str(object(pkg.engines)?.gutterpress) ? { gutterpressRange: str(object(pkg.engines)?.gutterpress) } : {},
3757
3948
  ..."preview" in gp ? { preview: str(gp.preview) ?? null } : {},
3758
3949
  ...Array.isArray(gp.styles) ? { styles: declaredList(gp.styles) } : {},
3759
3950
  ...str(gp.tokensFile) ? { tokensFile: str(gp.tokensFile) } : {},
@@ -3790,6 +3981,17 @@ function extensionCarries(meta) {
3790
3981
  function pathEscapesFolder(rel) {
3791
3982
  return path3.isAbsolute(rel) || rel.split(/[\\/]/).includes("..");
3792
3983
  }
3984
+ function assertSupportsThisGutterpress(meta, ref) {
3985
+ const range = meta.gutterpressRange;
3986
+ if (range === undefined)
3987
+ return;
3988
+ if (!validRange(range)) {
3989
+ throw new Error(`Extension "${ref}" has an invalid engines.gutterpress range "${range}".`);
3990
+ }
3991
+ if (!satisfies(PACKAGE_VERSION, range, { includePrerelease: true })) {
3992
+ throw new Error(`Extension "${ref}" needs Gutterpress ${range} (its package.json engines.gutterpress); ` + `this is Gutterpress ${PACKAGE_VERSION}. Update Gutterpress, or pin an older version of the extension.`);
3993
+ }
3994
+ }
3793
3995
  function assertExtensionContained(meta) {
3794
3996
  if (meta.engineStyles !== undefined) {
3795
3997
  throw new Error("The extension's metadata declares `engineStyles`, which was removed — " + "move its entries to the end of `styles`.");
@@ -4078,6 +4280,7 @@ This folder still has a ${stale}. Gutterpress no longer reads it — ` + `descri
4078
4280
  async function loadExtensionFromDir(extensionDir, config, pluginRef) {
4079
4281
  const meta = await readExtensionMeta(extensionDir);
4080
4282
  assertExtensionContained(meta);
4283
+ assertSupportsThisGutterpress(meta, pluginRef);
4081
4284
  const resolved = resolveExtension(extensionDir, meta, `Plugin "${pluginRef}"`);
4082
4285
  const extensionStyles = resolved.styles ?? [];
4083
4286
  const name = config.name ?? meta.name ?? pluginRef;
@@ -4152,6 +4355,7 @@ async function loadPlugin(config, baseDir) {
4152
4355
  if (loaded.packageDir) {
4153
4356
  const meta = await readExtensionMeta(loaded.packageDir);
4154
4357
  assertExtensionContained(meta);
4358
+ assertSupportsThisGutterpress(meta, pluginRef);
4155
4359
  const resolved = resolveExtension(loaded.packageDir, { ...meta, main: undefined }, `Plugin "${pluginRef}"`);
4156
4360
  if (resolved.markdown && loaded.entryPath && resolve3(resolved.markdown) !== resolve3(loaded.entryPath)) {
4157
4361
  throw new Error(`its package.json declares gutterpress.markdown "${meta.markdown}" but the package entry is "${relative(loaded.packageDir, loaded.entryPath)}" — an npm extension's ` + "markdown-it plugin is its package entry, so declare that file or drop the field.");
@@ -4282,7 +4486,7 @@ import path5 from "node:path";
4282
4486
  import { Transform } from "node:stream";
4283
4487
  import { pipeline } from "node:stream/promises";
4284
4488
  import { createGunzip } from "node:zlib";
4285
- import { maxSatisfying, satisfies, validRange } from "semver";
4489
+ import { maxSatisfying, satisfies as satisfies2, validRange as validRange2 } from "semver";
4286
4490
 
4287
4491
  // src/lib/npm-registry.ts
4288
4492
  var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
@@ -4490,7 +4694,7 @@ async function selectPackage(ctx, name, selector) {
4490
4694
  version = selector;
4491
4695
  } else if (typeof tags[selector] === "string") {
4492
4696
  version = tags[selector];
4493
- } else if (validRange(selector)) {
4697
+ } else if (validRange2(selector)) {
4494
4698
  version = maxSatisfying(Object.keys(versions), selector) ?? undefined;
4495
4699
  }
4496
4700
  if (!version || !isExactNpmVersion(version)) {
@@ -4708,7 +4912,7 @@ async function installPackage(ctx, name, selector, parentPackagePath, ancestors,
4708
4912
  }
4709
4913
  throw error2;
4710
4914
  }
4711
- const ancestor = ancestors.find((candidate) => candidate.name === name && (candidate.version === selected.version || (selector ? satisfies(candidate.version, selector) : false)));
4915
+ const ancestor = ancestors.find((candidate) => candidate.name === name && (candidate.version === selected.version || (selector ? satisfies2(candidate.version, selector) : false)));
4712
4916
  if (ancestor)
4713
4917
  return { path: ancestor.path };
4714
4918
  if (ctx.records.length >= ctx.limits.totalPackages) {
@@ -5767,8 +5971,8 @@ async function resolveGhostscript(targetPlatform = process.platform, env = proce
5767
5971
  }
5768
5972
  return targetPlatform === "win32" ? findWindowsGhostscriptInstall(env) : undefined;
5769
5973
  }
5770
- async function hasLiveTransparency(pdfBytes) {
5771
- const { PDFDocument, PDFDict, PDFName, PDFNumber, PDFRawStream, PDFRef } = await import("pdf-lib");
5974
+ async function transparentPages(pdfBytes) {
5975
+ const { PDFDocument, PDFDict, PDFName, PDFNumber, PDFRawStream, PDFArray, PDFRef } = await import("pdf-lib");
5772
5976
  let doc;
5773
5977
  try {
5774
5978
  doc = await PDFDocument.load(pdfBytes, {
@@ -5776,36 +5980,64 @@ async function hasLiveTransparency(pdfBytes) {
5776
5980
  throwOnInvalidObject: false
5777
5981
  });
5778
5982
  } catch {
5779
- return false;
5983
+ return [];
5780
5984
  }
5781
5985
  const ctx = doc.context;
5782
- const NONE = "/None";
5783
- for (const [, obj] of ctx.enumerateIndirectObjects()) {
5784
- const dict = obj instanceof PDFRawStream ? obj.dict : obj instanceof PDFDict ? obj : undefined;
5785
- if (!dict)
5786
- continue;
5986
+ const SKIP = new Set(["Parent", "Font", "Annots"]);
5987
+ const isSet = (v) => v !== undefined && String(v) !== "/None";
5988
+ const dictHasTransparency = (dict) => {
5787
5989
  const smask = dict.get(PDFName.of("SMask"));
5788
5990
  const subtype = String(dict.get(PDFName.of("Subtype")) ?? "");
5789
5991
  const type = String(dict.get(PDFName.of("Type")) ?? "");
5790
- if (subtype === "/Image" && smask && String(smask) !== NONE)
5992
+ if (subtype === "/Image" && isSet(smask))
5791
5993
  return true;
5792
- if (type === "/ExtGState") {
5793
- if (smask && String(smask) !== NONE)
5994
+ if (type === "/ExtGState" || dict.has(PDFName.of("ca")) || dict.has(PDFName.of("BM"))) {
5995
+ if (isSet(smask))
5794
5996
  return true;
5795
5997
  for (const key of ["ca", "CA"]) {
5796
- const alpha = dict.get(PDFName.of(key));
5998
+ const alpha = ctx.lookup(dict.get(PDFName.of(key)));
5797
5999
  if (alpha instanceof PDFNumber && alpha.asNumber() < 1)
5798
6000
  return true;
5799
6001
  }
6002
+ const bm = ctx.lookup(dict.get(PDFName.of("BM")));
6003
+ const mode = bm instanceof PDFArray ? bm.get(0) : bm;
6004
+ if (mode && !["/Normal", "/Compatible"].includes(String(mode)))
6005
+ return true;
5800
6006
  }
5801
- const group = dict.get(PDFName.of("Group"));
5802
- if (group) {
5803
- const resolved = group instanceof PDFRef ? ctx.lookup(group) : group;
5804
- if (resolved instanceof PDFDict && String(resolved.get(PDFName.of("S"))) === "/Transparency")
6007
+ const group = ctx.lookup(dict.get(PDFName.of("Group")));
6008
+ return group instanceof PDFDict && String(group.get(PDFName.of("S"))) === "/Transparency";
6009
+ };
6010
+ const reaches = (root) => {
6011
+ const seen = new Set;
6012
+ const stack = [root];
6013
+ while (stack.length) {
6014
+ const node = stack.pop();
6015
+ const obj = node instanceof PDFRef ? ctx.lookup(node) : node;
6016
+ if (!obj || seen.has(obj))
6017
+ continue;
6018
+ seen.add(obj);
6019
+ if (obj instanceof PDFArray) {
6020
+ stack.push(...obj.asArray());
6021
+ continue;
6022
+ }
6023
+ const dict = obj instanceof PDFRawStream ? obj.dict : obj instanceof PDFDict ? obj : undefined;
6024
+ if (!dict)
6025
+ continue;
6026
+ if (dictHasTransparency(dict))
5805
6027
  return true;
6028
+ for (const [key, value] of dict.entries()) {
6029
+ if (!SKIP.has(key.asString().slice(1)))
6030
+ stack.push(value);
6031
+ }
5806
6032
  }
5807
- }
5808
- return false;
6033
+ return false;
6034
+ };
6035
+ const pages = [];
6036
+ doc.getPages().forEach((page, i) => {
6037
+ if (reaches(page.node))
6038
+ pages.push(i + 1);
6039
+ });
6040
+ return pages;
5809
6041
  }
5810
6042
  async function findPostScriptShadingPages(pdfBytes) {
5811
6043
  const { PDFDocument, PDFDict, PDFName, PDFNumber, PDFRawStream, PDFRef } = await import("pdf-lib");
@@ -5876,6 +6108,75 @@ async function stripAnnotations(pdfPath, stagingDir) {
5876
6108
  const out = await doc.save();
5877
6109
  await writeFile3(pdfPath, out);
5878
6110
  }
6111
+ function planTransparentPages(transparent, reasons, flattenPages, gradientPages) {
6112
+ const split = [];
6113
+ const artOnly = [];
6114
+ const kept = new Map;
6115
+ for (const n of transparent) {
6116
+ const reason = reasons.get(n);
6117
+ if (reason === "no-text")
6118
+ artOnly.push(n);
6119
+ else if (flattenPages.includes(n))
6120
+ kept.set(n, "listed");
6121
+ else if (reason)
6122
+ kept.set(n, reason);
6123
+ else if (gradientPages.includes(n))
6124
+ kept.set(n, "gradient");
6125
+ else
6126
+ split.push(n);
6127
+ }
6128
+ return { split, artOnly, kept };
6129
+ }
6130
+ async function flattenTransparentPages(pdfPath, pageNumbers, stagingDir, dpi = 300) {
6131
+ const pages = [...new Set(pageNumbers)].sort((a, b) => a - b);
6132
+ if (pages.length === 0)
6133
+ return;
6134
+ const ghostscript = await resolveGhostscript();
6135
+ if (!ghostscript) {
6136
+ throw new Error("Ghostscript executable not found. Install Ghostscript or set GHOSTSCRIPT_PATH to its command-line executable.");
6137
+ }
6138
+ const art = join3(stagingDir, "flatten-art.pdf");
6139
+ const text = join3(stagingDir, "flatten-text.pdf");
6140
+ const pageList = `-sPageList=${pages.join(",")}`;
6141
+ const gs = (...args) => run(ghostscript, [
6142
+ "-q",
6143
+ "-dBATCH",
6144
+ "-dNOPAUSE",
6145
+ "-dSAFER",
6146
+ "-dPreserveAnnots=false",
6147
+ "-dNO_PDFMARK_OUTLINES",
6148
+ ...args
6149
+ ]);
6150
+ try {
6151
+ await gs(pageList, "-sDEVICE=pdfimage24", "-dFILTERTEXT", `-r${dpi}`, `-sOutputFile=${art}`, pdfPath);
6152
+ await gs(pageList, "-sDEVICE=pdfwrite", "-dNOTRANSPARENCY", "-dFILTERIMAGE", "-dFILTERVECTOR", "-dCompatibilityLevel=1.3", `-sOutputFile=${text}`, pdfPath);
6153
+ const { PDFDocument, PDFName } = await import("pdf-lib");
6154
+ const doc = await PDFDocument.load(await readFile9(pdfPath), { updateMetadata: false });
6155
+ const artDoc = await PDFDocument.load(await readFile9(art));
6156
+ const textDoc = await PDFDocument.load(await readFile9(text));
6157
+ if (artDoc.getPageCount() !== pages.length || textDoc.getPageCount() !== pages.length) {
6158
+ throw new Error(`Ghostscript returned ${artDoc.getPageCount()} artwork and ${textDoc.getPageCount()} text pages for ${pages.length} transparent pages.`);
6159
+ }
6160
+ const artPages = await doc.embedPages(artDoc.getPages());
6161
+ const withText = textDoc.getPages().filter((p) => p.node.Contents());
6162
+ const embeddedText = await doc.embedPages(withText);
6163
+ const textPages = textDoc.getPages().map((p) => embeddedText[withText.indexOf(p)]);
6164
+ pages.forEach((pageNumber, i) => {
6165
+ const page = doc.getPage(pageNumber - 1);
6166
+ const { x, y, width, height } = page.getMediaBox();
6167
+ page.node.set(PDFName.of("Contents"), doc.context.obj([]));
6168
+ page.node.set(PDFName.of("Resources"), doc.context.obj({}));
6169
+ page.node.delete(PDFName.of("Group"));
6170
+ page.drawPage(artPages[i], { x, y, width, height });
6171
+ const textPage = textPages[i];
6172
+ if (textPage)
6173
+ page.drawPage(textPage, { x, y, width, height });
6174
+ });
6175
+ await writeFile3(pdfPath, await doc.save());
6176
+ } finally {
6177
+ await Promise.all([art, text].map((f) => unlink2(f).catch(() => {})));
6178
+ }
6179
+ }
5879
6180
  function makePdfxDefPs(params) {
5880
6181
  const pdfxVersion = params.pdfx === "x3" ? "PDF/X-3:2002" : "PDF/X-1:2001";
5881
6182
  const pdfxConformance = params.pdfx === "x1a" ? "PDF/X-1a:2001" : null;
@@ -5967,6 +6268,10 @@ async function convertToPdfxCmyk(inputPdf, outPdf, config) {
5967
6268
  "-dDownsampleColorImages=false",
5968
6269
  "-dDownsampleGrayImages=false",
5969
6270
  "-dDownsampleMonoImages=false",
6271
+ "-dAutoFilterColorImages=false",
6272
+ "-dColorImageFilter=/FlateEncode",
6273
+ "-dAutoFilterGrayImages=false",
6274
+ "-dGrayImageFilter=/FlateEncode",
5970
6275
  ...config.rasterDpi ? [`-r${config.rasterDpi}`] : [],
5971
6276
  "-sProcessColorModel=DeviceCMYK",
5972
6277
  "-sColorConversionStrategy=CMYK",
@@ -6014,117 +6319,6 @@ import { mkdir as mkdir5, readFile as readFile10, writeFile as writeFile4 } from
6014
6319
  import { createHash as createHash3 } from "node:crypto";
6015
6320
  import path10 from "node:path";
6016
6321
  import git from "isomorphic-git";
6017
- // package.json
6018
- var package_default = {
6019
- name: "gutterpress",
6020
- version: "0.11.20-alpha.2",
6021
- description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
6022
- author: "itlackey",
6023
- license: "MPL-2.0",
6024
- keywords: [
6025
- "markdown",
6026
- "pdf",
6027
- "print",
6028
- "ghostscript",
6029
- "book",
6030
- "publishing",
6031
- "zine"
6032
- ],
6033
- repository: {
6034
- type: "git",
6035
- url: "https://github.com/dimm-city/gutterpress.git"
6036
- },
6037
- homepage: "https://github.com/dimm-city/gutterpress#readme",
6038
- bugs: {
6039
- url: "https://github.com/dimm-city/gutterpress/issues"
6040
- },
6041
- type: "module",
6042
- main: "./dist/index.js",
6043
- types: "./dist/index.d.ts",
6044
- bin: {
6045
- gutterpress: "./dist/cli.js"
6046
- },
6047
- exports: {
6048
- ".": {
6049
- types: "./dist/index.d.ts",
6050
- default: "./dist/index.js"
6051
- },
6052
- "./api": {
6053
- types: "./dist/api/index.d.ts",
6054
- default: "./dist/api/index.js"
6055
- },
6056
- "./render": {
6057
- types: "./dist/render.d.ts",
6058
- default: "./dist/render.js"
6059
- }
6060
- },
6061
- files: [
6062
- "dist/",
6063
- "README.md",
6064
- "LICENSE"
6065
- ],
6066
- scripts: {
6067
- "build:library": "bun scripts/build-engine-bundles.mjs && rimraf dist && bun build src/index.ts src/api/index.ts --target=node --packages=external --splitting --outdir=dist && bun build src/render.ts --target=node --packages=external --outdir=dist && node scripts/check-render-pure.mjs && tsc -p tsconfig.build.json",
6068
- build: "bun run build:library && bun build src/cli.ts --target=node --packages=external --splitting --outdir=dist",
6069
- prepublishOnly: "bun run build",
6070
- test: "bun test",
6071
- typecheck: "tsc --noEmit",
6072
- "typecheck:engine-browser": "tsc --noEmit -p src/engine/browser.tsconfig.json",
6073
- "parity:gate": "bun scripts/native-parity-gate.ts"
6074
- },
6075
- engines: {
6076
- bun: ">=1.0.0",
6077
- node: ">=22.0.0"
6078
- },
6079
- dependencies: {
6080
- chokidar: "^5.0.0",
6081
- citty: "^0.2.0",
6082
- diff3: "0.0.3",
6083
- fflate: "0.8.3",
6084
- glob: "^13.0.0",
6085
- htmlhint: "^1.9.2",
6086
- ignore: "^5.3.2",
6087
- "isomorphic-git": "^1.38.4",
6088
- "markdown-it": "^14.3.2",
6089
- "markdown-it-abbr": "^2.0.0",
6090
- "markdown-it-attrs": "^4.3.0",
6091
- "markdown-it-deflist": "^3.0.1",
6092
- "markdown-it-footnote": "^4.0.0",
6093
- "markdown-it-mark": "^4.0.0",
6094
- "markdown-it-source-map": "^0.1.1",
6095
- "markdown-it-sub": "^2.0.0",
6096
- "markdown-it-sup": "^2.0.0",
6097
- markdownlint: "^0.40.0",
6098
- "pdf-lib": "^1.17.1",
6099
- postcss: "^8.5.18",
6100
- "resolve.exports": "2.0.3",
6101
- semver: "7.8.0",
6102
- tar: "7.5.22",
6103
- unpdf: "^1.6.2",
6104
- ws: "^8.21.0",
6105
- yaml: "^2.8.2"
6106
- },
6107
- devDependencies: {
6108
- "@types/bun": "1.3.14",
6109
- "@types/markdown-it": "^14.1.2",
6110
- "@types/node": "^22.0.0",
6111
- "@types/semver": "^7.7.1",
6112
- "@types/ws": "^8.5.13",
6113
- playwright: "^1.60.0",
6114
- rimraf: "^6.0.0",
6115
- typescript: "^5"
6116
- }
6117
- };
6118
-
6119
- // src/lib/version.ts
6120
- var PACKAGE_META = {
6121
- version: package_default.version ?? "unknown",
6122
- dependencies: package_default.dependencies ?? {},
6123
- devDependencies: package_default.devDependencies ?? {}
6124
- };
6125
- var PACKAGE_VERSION = PACKAGE_META.version;
6126
-
6127
- // src/lib/build-fingerprint.ts
6128
6322
  var FINGERPRINT_FILENAME = "build-fingerprint.json";
6129
6323
  var VERSION_TIMEOUT_MS = 4000;
6130
6324
  function toJsonValue(value) {
@@ -6272,7 +6466,10 @@ async function writeBuildFingerprint(input) {
6272
6466
  resolvedFlavor: input.pdfx.resolvedFlavor,
6273
6467
  iccPath: input.pdfx.iccPath,
6274
6468
  stripAnnotations: input.pdfx.stripAnnotations,
6275
- blackText: input.pdfx.blackText ?? null
6469
+ blackText: input.pdfx.blackText ?? null,
6470
+ transparency: input.pdfx.transparency ?? null,
6471
+ flattenPages: input.pdfx.flattenPages ?? null,
6472
+ rasterDpi: input.pdfx.rasterDpi ?? null
6276
6473
  },
6277
6474
  outputDir: input.recordedOutputDir ? path10.resolve(input.recordedOutputDir) : outputDir
6278
6475
  },
@@ -6614,346 +6811,6 @@ function inspectionFailed(checkId, message, extra = {}) {
6614
6811
  };
6615
6812
  }
6616
6813
 
6617
- // src/lib/pdf-inspect.ts
6618
- import { stat as stat2, readFile as readFile13 } from "node:fs/promises";
6619
- import { getDocumentProxy } from "unpdf";
6620
- import * as pdfjs from "unpdf/pdfjs";
6621
- function getOps(mod) {
6622
- if ("OPS" in mod && mod.OPS && typeof mod.OPS === "object") {
6623
- return mod.OPS;
6624
- }
6625
- throw new Error("unpdf/pdfjs missing OPS export");
6626
- }
6627
- var OPS = getOps(pdfjs);
6628
- var docCache = new Map;
6629
- var DOC_CACHE_MAX = 8;
6630
- function destroyEntry(entry) {
6631
- entry.doc.then((d) => d.destroy()).catch(() => {});
6632
- }
6633
- var EVICT_DESTROY_GRACE_MS = 60000;
6634
- var gracePending = new Map;
6635
- function destroyEntryAfterGrace(entry) {
6636
- const t = setTimeout(() => {
6637
- gracePending.delete(t);
6638
- destroyEntry(entry);
6639
- }, EVICT_DESTROY_GRACE_MS);
6640
- t.unref?.();
6641
- gracePending.set(t, entry);
6642
- }
6643
- function evictLru() {
6644
- while (docCache.size > DOC_CACHE_MAX) {
6645
- const oldestKey = docCache.keys().next().value;
6646
- if (oldestKey === undefined)
6647
- break;
6648
- const evicted = docCache.get(oldestKey);
6649
- docCache.delete(oldestKey);
6650
- if (evicted)
6651
- destroyEntryAfterGrace(evicted);
6652
- }
6653
- }
6654
- async function loadPdf(path11) {
6655
- let st;
6656
- try {
6657
- st = await stat2(path11);
6658
- } catch {
6659
- return null;
6660
- }
6661
- const hit = docCache.get(path11);
6662
- if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size) {
6663
- docCache.delete(path11);
6664
- docCache.set(path11, hit);
6665
- try {
6666
- return await hit.doc;
6667
- } catch {
6668
- return null;
6669
- }
6670
- }
6671
- if (hit)
6672
- destroyEntry(hit);
6673
- const docPromise = readFile13(path11).then((buf) => getDocumentProxy(new Uint8Array(buf)));
6674
- docCache.delete(path11);
6675
- docCache.set(path11, { mtimeMs: st.mtimeMs, size: st.size, doc: docPromise });
6676
- evictLru();
6677
- try {
6678
- return await docPromise;
6679
- } catch {
6680
- if (docCache.get(path11)?.doc === docPromise)
6681
- docCache.delete(path11);
6682
- return null;
6683
- }
6684
- }
6685
- var activeRetains = 0;
6686
- function retainPdfCache() {
6687
- activeRetains++;
6688
- let released = false;
6689
- return () => {
6690
- if (released)
6691
- return;
6692
- released = true;
6693
- activeRetains--;
6694
- if (activeRetains === 0)
6695
- clearPdfCache();
6696
- };
6697
- }
6698
- function clearPdfCache() {
6699
- for (const [t, entry] of gracePending) {
6700
- clearTimeout(t);
6701
- destroyEntry(entry);
6702
- }
6703
- gracePending.clear();
6704
- for (const entry of docCache.values())
6705
- destroyEntry(entry);
6706
- docCache.clear();
6707
- }
6708
- function getPageSize(page) {
6709
- const vp = page.getViewport({ scale: 1 });
6710
- return { w: vp.width, h: vp.height };
6711
- }
6712
- async function getOutlineCount(doc) {
6713
- try {
6714
- const outline = await doc.getOutline();
6715
- return outline ? outline.length : 0;
6716
- } catch {
6717
- return 0;
6718
- }
6719
- }
6720
- async function getPageLabels(doc) {
6721
- try {
6722
- return await doc.getPageLabels();
6723
- } catch {
6724
- return null;
6725
- }
6726
- }
6727
- async function countLinkAnnotations(doc, stopAtFirst = false) {
6728
- let count = 0;
6729
- for (let i = 1;i <= doc.numPages; i++) {
6730
- try {
6731
- const page = await doc.getPage(i);
6732
- const annots = await page.getAnnotations();
6733
- for (const a of annots) {
6734
- if (a.subtype === "Link") {
6735
- count++;
6736
- if (stopAtFirst)
6737
- return count;
6738
- }
6739
- }
6740
- } catch {}
6741
- }
6742
- return count;
6743
- }
6744
- var textCache = new WeakMap;
6745
- function getTextPass(doc) {
6746
- let p = textCache.get(doc);
6747
- if (!p) {
6748
- p = (async () => {
6749
- const textByPage = [];
6750
- const positions = [];
6751
- const runsByPage = [];
6752
- for (let i = 1;i <= doc.numPages; i++) {
6753
- try {
6754
- const page = await doc.getPage(i);
6755
- const tc = await page.getTextContent();
6756
- let pageText = "";
6757
- const runs = [];
6758
- for (const item of tc.items) {
6759
- const it = item;
6760
- if (typeof it.str !== "string")
6761
- continue;
6762
- pageText += it.str;
6763
- if (it.transform && it.str.trim().length > 0) {
6764
- const x = it.transform[4];
6765
- const y = it.transform[5];
6766
- positions.push({ x, y });
6767
- runs.push({ s: it.str, x, y, w: it.width ?? 0, h: it.height ?? 0 });
6768
- }
6769
- }
6770
- textByPage.push(pageText);
6771
- runsByPage.push(runs);
6772
- } catch {
6773
- textByPage.push("");
6774
- runsByPage.push([]);
6775
- }
6776
- }
6777
- return { textByPage, positions, runsByPage };
6778
- })();
6779
- textCache.set(doc, p);
6780
- }
6781
- return p;
6782
- }
6783
- var opCache = new WeakMap;
6784
- function multiply(m1, m2) {
6785
- return [
6786
- m1[0] * m2[0] + m1[2] * m2[1],
6787
- m1[1] * m2[0] + m1[3] * m2[1],
6788
- m1[0] * m2[2] + m1[2] * m2[3],
6789
- m1[1] * m2[2] + m1[3] * m2[3],
6790
- m1[0] * m2[4] + m1[2] * m2[5] + m1[4],
6791
- m1[1] * m2[4] + m1[3] * m2[5] + m1[5]
6792
- ];
6793
- }
6794
- function fontIsEmbedded(f) {
6795
- if (f.isType3Font)
6796
- return true;
6797
- if (f.missingFile === true)
6798
- return false;
6799
- if (f.isStandardFont === true)
6800
- return false;
6801
- return true;
6802
- }
6803
- function getOpPass(doc) {
6804
- let p = opCache.get(doc);
6805
- if (!p) {
6806
- p = (async () => {
6807
- const imagesByPage = new Map;
6808
- const fonts = [];
6809
- const seenFonts = new Set;
6810
- for (let i = 1;i <= doc.numPages; i++) {
6811
- let page;
6812
- try {
6813
- page = await doc.getPage(i);
6814
- } catch {
6815
- continue;
6816
- }
6817
- let ops;
6818
- try {
6819
- ops = await page.getOperatorList();
6820
- } catch {
6821
- continue;
6822
- }
6823
- let ctm = [1, 0, 0, 1, 0, 0];
6824
- const stack = [];
6825
- const pageImages = [];
6826
- for (let j = 0;j < ops.fnArray.length; j++) {
6827
- const fn = ops.fnArray[j];
6828
- const args = ops.argsArray[j];
6829
- if (fn === OPS.save) {
6830
- stack.push(ctm.slice());
6831
- } else if (fn === OPS.restore) {
6832
- ctm = stack.pop() ?? ctm;
6833
- } else if (fn === OPS.transform) {
6834
- ctm = multiply(ctm, args);
6835
- } else if (fn === OPS.paintImageXObject || fn === OPS.paintImageXObjectRepeat || fn === OPS.paintInlineImageXObject) {
6836
- const name = typeof args[0] === "string" ? args[0] : "(inline)";
6837
- pageImages.push({
6838
- name,
6839
- placedW: Math.hypot(ctm[0], ctm[1]),
6840
- placedH: Math.hypot(ctm[2], ctm[3]),
6841
- x: ctm[4],
6842
- y: ctm[5],
6843
- page: i
6844
- });
6845
- } else if (fn === OPS.setFont) {
6846
- const ref = args[0];
6847
- if (typeof ref === "string" && !seenFonts.has(ref)) {
6848
- seenFonts.add(ref);
6849
- try {
6850
- if (page.commonObjs.has(ref)) {
6851
- const f = page.commonObjs.get(ref);
6852
- fonts.push({
6853
- name: f?.name ?? ref,
6854
- embedded: fontIsEmbedded(f ?? {})
6855
- });
6856
- }
6857
- } catch {}
6858
- }
6859
- }
6860
- }
6861
- if (pageImages.length > 0)
6862
- imagesByPage.set(i, pageImages);
6863
- }
6864
- return { imagesByPage, fonts };
6865
- })();
6866
- opCache.set(doc, p);
6867
- }
6868
- return p;
6869
- }
6870
- function getImagePixelSize(page, name, timeoutMs = 2000) {
6871
- return new Promise((resolve6) => {
6872
- let done = false;
6873
- const finish = (v) => {
6874
- if (!done) {
6875
- done = true;
6876
- resolve6(v);
6877
- }
6878
- };
6879
- const t = setTimeout(() => finish(null), timeoutMs);
6880
- try {
6881
- page.objs.get(name, (obj) => {
6882
- clearTimeout(t);
6883
- finish(obj && obj.width && obj.height ? { width: obj.width, height: obj.height } : null);
6884
- });
6885
- } catch {
6886
- clearTimeout(t);
6887
- finish(null);
6888
- }
6889
- });
6890
- }
6891
- var PAINT_OPS = new Set([
6892
- "stroke",
6893
- "closeStroke",
6894
- "fill",
6895
- "eoFill",
6896
- "fillStroke",
6897
- "eoFillStroke",
6898
- "closeFillStroke",
6899
- "closeEOFillStroke",
6900
- "shadingFill",
6901
- "paintImageXObject",
6902
- "paintInlineImageXObject",
6903
- "paintImageXObjectRepeat",
6904
- "paintImageMaskXObject",
6905
- "paintImageMaskXObjectGroup",
6906
- "paintImageMaskXObjectRepeat",
6907
- "paintSolidColorImageMask",
6908
- "showText",
6909
- "showSpacedText",
6910
- "nextLineShowText",
6911
- "nextLineSetSpacingShowText"
6912
- ].map((name) => OPS[name]).filter((op) => op !== undefined));
6913
- async function pageHasMarks(doc, pageNum) {
6914
- const ops = await (await doc.getPage(pageNum)).getOperatorList();
6915
- return ops.fnArray.some((fn, i) => fn === OPS.constructPath ? PAINT_OPS.has(ops.argsArray[i][0]) : PAINT_OPS.has(fn));
6916
- }
6917
- async function getImageResolutions(doc) {
6918
- const { imagesByPage } = await getOpPass(doc);
6919
- const out = [];
6920
- for (const [pageNum, imgs] of imagesByPage) {
6921
- let page;
6922
- try {
6923
- page = await doc.getPage(pageNum);
6924
- } catch {
6925
- continue;
6926
- }
6927
- for (const img of imgs) {
6928
- if (img.name === "(inline)")
6929
- continue;
6930
- const px = await getImagePixelSize(page, img.name);
6931
- if (!px)
6932
- continue;
6933
- const placedWin = img.placedW / 72;
6934
- const placedHin = img.placedH / 72;
6935
- if (placedWin <= 0 || placedHin <= 0)
6936
- continue;
6937
- out.push({
6938
- page: pageNum,
6939
- xDpi: px.width / placedWin,
6940
- yDpi: px.height / placedHin
6941
- });
6942
- }
6943
- }
6944
- return out;
6945
- }
6946
- async function isLoadable(doc) {
6947
- try {
6948
- for (let i = 1;i <= doc.numPages; i++) {
6949
- await doc.getPage(i);
6950
- }
6951
- return true;
6952
- } catch {
6953
- return false;
6954
- }
6955
- }
6956
-
6957
6814
  // src/checks/pdf/qpdf-structure.ts
6958
6815
  var check2 = {
6959
6816
  id: "pdf.structure.qpdf",
@@ -7228,9 +7085,9 @@ var check5 = {
7228
7085
  registerCheck(check5);
7229
7086
 
7230
7087
  // src/lib/pdf-parse.ts
7231
- import { readFile as readFile14 } from "node:fs/promises";
7088
+ import { readFile as readFile13 } from "node:fs/promises";
7232
7089
  async function readPdfBytes(pdfPath) {
7233
- return readFile14(pdfPath, "latin1");
7090
+ return readFile13(pdfPath, "latin1");
7234
7091
  }
7235
7092
  function parseInkCov(out) {
7236
7093
  const lines = out.split(/\r?\n/).filter((l) => l.includes("CMYK"));
@@ -7491,7 +7348,7 @@ var check9 = {
7491
7348
  registerCheck(check9);
7492
7349
 
7493
7350
  // src/checks/pdf/black-text.ts
7494
- import { readFile as readFile15 } from "node:fs/promises";
7351
+ import { readFile as readFile14 } from "node:fs/promises";
7495
7352
  var check10 = {
7496
7353
  id: "pdf.print.black-text",
7497
7354
  name: "Black Text",
@@ -7504,7 +7361,7 @@ var check10 = {
7504
7361
  return [];
7505
7362
  try {
7506
7363
  const { findRichBlackText, LARGE_TEXT_PT } = await import("./black-text-gray-7dz61456.js");
7507
- const found = await findRichBlackText(await readFile15(ctx.pdfPath));
7364
+ const found = await findRichBlackText(await readFile14(ctx.pdfPath));
7508
7365
  if (found.runs === 0)
7509
7366
  return [];
7510
7367
  return [
@@ -7894,7 +7751,7 @@ registerCheck(check19);
7894
7751
 
7895
7752
  // src/checks/source/htmlhint.ts
7896
7753
  import { existsSync as existsSync9 } from "node:fs";
7897
- import { readFile as readFile16 } from "node:fs/promises";
7754
+ import { readFile as readFile15 } from "node:fs/promises";
7898
7755
  import htmlhint from "htmlhint";
7899
7756
  var { HTMLHint } = htmlhint;
7900
7757
  var CONFIG_NAMES2 = [".htmlhintrc"];
@@ -7914,8 +7771,8 @@ var check20 = {
7914
7771
  const resolvedConfig = findConfigFile(ctx.inputDir, CONFIG_NAMES2, configPath);
7915
7772
  if (!resolvedConfig && !configPath)
7916
7773
  return [];
7917
- const ruleset = resolvedConfig ? JSON.parse(await readFile16(resolvedConfig, "utf8")) : HTMLHint.defaultRuleset;
7918
- const html = await readFile16(ctx.htmlPath, "utf8");
7774
+ const ruleset = resolvedConfig ? JSON.parse(await readFile15(resolvedConfig, "utf8")) : HTMLHint.defaultRuleset;
7775
+ const html = await readFile15(ctx.htmlPath, "utf8");
7919
7776
  const messages = HTMLHint.verify(html, ruleset);
7920
7777
  return messages.map((m) => ({
7921
7778
  checkId: check20.id,
@@ -7930,7 +7787,7 @@ var check20 = {
7930
7787
  registerCheck(check20);
7931
7788
 
7932
7789
  // src/checks/source/stylelint.ts
7933
- import { readFile as readFile17 } from "node:fs/promises";
7790
+ import { readFile as readFile16 } from "node:fs/promises";
7934
7791
 
7935
7792
  // src/lib/printsafe.ts
7936
7793
  import postcss2 from "postcss";
@@ -8272,7 +8129,7 @@ function checkCssRaw(css, from) {
8272
8129
  warnings.push({
8273
8130
  rule: ruleRiskyProps,
8274
8131
  severity: "warning",
8275
- message: `"${decl.prop}: ${decl.value}" prints blank in PDF/X exports (--format pdfx): Chromium emits repeating-*-gradient() and conic-gradient() in a form Ghostscript cannot convert. Plain PDF output is unaffected. Use linear-gradient() or radial-gradient() instead; for 0deg/90deg stripes write "linear-gradient(90deg, #b33 0 6px, #c8c5bf 6px 10px)" with "background-size: 10px 100%", and for anything else a small tiled image.`,
8132
+ message: `"${decl.prop}: ${decl.value}" is at risk in PDF/X exports (--format pdfx): Chromium emits repeating-*-gradient() and conic-gradient() in a form Ghostscript has been seen to paint as nothing or as irregular stripes, so it can print blank or distorted, unless the page also uses transparency and Ghostscript flattens the whole page to an image, which keeps the gradient but loses that page's live text. Plain PDF output is unaffected. Use linear-gradient() or radial-gradient() instead; for 0deg/90deg stripes write "linear-gradient(90deg, #b33 0 6px, #c8c5bf 6px 10px)" with "background-size: 10px 100%", and for anything else a small tiled image.`,
8276
8133
  ...nodeLoc(decl)
8277
8134
  });
8278
8135
  } else if (isUnpairedFitContentWidth(decl)) {
@@ -8337,7 +8194,7 @@ var check21 = {
8337
8194
  for (const file of files) {
8338
8195
  let css;
8339
8196
  try {
8340
- css = await readFile17(file, "utf8");
8197
+ css = await readFile16(file, "utf8");
8341
8198
  } catch (err) {
8342
8199
  results.push({
8343
8200
  checkId: check21.id,
@@ -8364,12 +8221,12 @@ var check21 = {
8364
8221
  registerCheck(check21);
8365
8222
 
8366
8223
  // src/checks/source/css-ownership.ts
8367
- import { readFile as readFile18 } from "node:fs/promises";
8224
+ import { readFile as readFile17 } from "node:fs/promises";
8368
8225
  import postcss3 from "postcss";
8369
8226
  import { parse as parseYaml3 } from "yaml";
8370
8227
  var CONFIG_NAMES3 = [".gutterpress/css-contract.yaml", ".gutterpress/css-contract.yml"];
8371
8228
  async function loadContract(configPath) {
8372
- const raw = await readFile18(configPath, "utf8");
8229
+ const raw = await readFile17(configPath, "utf8");
8373
8230
  const parsed = parseYaml3(raw);
8374
8231
  if (parsed && typeof parsed === "object")
8375
8232
  return parsed;
@@ -8520,7 +8377,7 @@ var check22 = {
8520
8377
  continue;
8521
8378
  let css;
8522
8379
  try {
8523
- css = await readFile18(absFile, "utf8");
8380
+ css = await readFile17(absFile, "utf8");
8524
8381
  } catch {
8525
8382
  continue;
8526
8383
  }
@@ -8566,7 +8423,7 @@ import { existsSync as existsSync10 } from "node:fs";
8566
8423
  import { dirname as dirname3, resolve as resolve6 } from "node:path";
8567
8424
 
8568
8425
  // src/checks/source/local-ref-parser.ts
8569
- import { readFile as readFile19 } from "node:fs/promises";
8426
+ import { readFile as readFile18 } from "node:fs/promises";
8570
8427
  function newlineOffsets(content) {
8571
8428
  const offsets = [];
8572
8429
  for (let i = 0;i < content.length; i++)
@@ -8635,7 +8492,7 @@ async function* renderedLocalRefs(ctx, checkId, results) {
8635
8492
  for (const file of files) {
8636
8493
  let refs;
8637
8494
  try {
8638
- refs = collectRenderedLocalRefs(await readFile19(file, "utf8"));
8495
+ refs = collectRenderedLocalRefs(await readFile18(file, "utf8"));
8639
8496
  } catch {
8640
8497
  results.push(inspectionFailed(checkId, `Could not read source file: ${file}`, { file }));
8641
8498
  continue;
@@ -8986,7 +8843,7 @@ var check24 = {
8986
8843
  registerCheck(check24);
8987
8844
 
8988
8845
  // src/checks/source/accessibility-alt-text.ts
8989
- import { readFile as readFile20 } from "node:fs/promises";
8846
+ import { readFile as readFile19 } from "node:fs/promises";
8990
8847
  var check25 = {
8991
8848
  id: "source.accessibility.alt-text",
8992
8849
  name: "Image Alt Text",
@@ -9000,7 +8857,7 @@ var check25 = {
9000
8857
  const results = [];
9001
8858
  for (const file of files) {
9002
8859
  try {
9003
- const content = await readFile20(file, "utf8");
8860
+ const content = await readFile19(file, "utf8");
9004
8861
  const lines = content.split(`
9005
8862
  `);
9006
8863
  let inFence = false;
@@ -9044,7 +8901,7 @@ function extractImageAlts(line) {
9044
8901
  registerCheck(check25);
9045
8902
 
9046
8903
  // src/checks/source/accessibility-heading-order.ts
9047
- import { readFile as readFile21 } from "node:fs/promises";
8904
+ import { readFile as readFile20 } from "node:fs/promises";
9048
8905
  var check26 = {
9049
8906
  id: "source.accessibility.heading-order",
9050
8907
  name: "Heading Order",
@@ -9058,7 +8915,7 @@ var check26 = {
9058
8915
  const results = [];
9059
8916
  for (const file of files) {
9060
8917
  try {
9061
- const content = await readFile21(file, "utf8");
8918
+ const content = await readFile20(file, "utf8");
9062
8919
  const lines = content.split(`
9063
8920
  `);
9064
8921
  let inFence = false;
@@ -9100,7 +8957,7 @@ var check26 = {
9100
8957
  registerCheck(check26);
9101
8958
 
9102
8959
  // src/checks/source/layout-markers.ts
9103
- import { readFile as readFile22 } from "node:fs/promises";
8960
+ import { readFile as readFile21 } from "node:fs/promises";
9104
8961
 
9105
8962
  // src/lib/unknown-classes.ts
9106
8963
  import postcss4 from "postcss";
@@ -9203,7 +9060,7 @@ async function collectKnownClasses(ctx, plugins) {
9203
9060
  const sheets = (await collectStyleDependencies(ctx.inputDir, roots)).filter((f) => /\.css$/i.test(f));
9204
9061
  for (const sheet of sheets) {
9205
9062
  try {
9206
- addCssClasses(await readFile22(sheet, "utf8"), names);
9063
+ addCssClasses(await readFile21(sheet, "utf8"), names);
9207
9064
  } catch {}
9208
9065
  }
9209
9066
  return names;
@@ -9227,7 +9084,7 @@ var check27 = {
9227
9084
  const knownClasses = () => known ??= collectKnownClasses(ctx, plugins);
9228
9085
  for (const file of files) {
9229
9086
  try {
9230
- const content = await readFile22(file, "utf8");
9087
+ const content = await readFile21(file, "utf8");
9231
9088
  const env = {};
9232
9089
  const tokens = md.parse(content, env);
9233
9090
  md.renderer.render(tokens, md.options, env);
@@ -9267,7 +9124,7 @@ var check27 = {
9267
9124
  registerCheck(check27);
9268
9125
 
9269
9126
  // src/checks/source/merge-markers.ts
9270
- import { readFile as readFile23 } from "node:fs/promises";
9127
+ import { readFile as readFile22 } from "node:fs/promises";
9271
9128
  import path12 from "node:path";
9272
9129
 
9273
9130
  // src/checks/asset/extensions.ts
@@ -9309,7 +9166,7 @@ var check28 = {
9309
9166
  for (const file of textFiles) {
9310
9167
  let content;
9311
9168
  try {
9312
- content = await readFile23(file, "utf8");
9169
+ content = await readFile22(file, "utf8");
9313
9170
  } catch {
9314
9171
  results.push(inspectionFailed(check28.id, `Could not read source file: ${file}`, { file }));
9315
9172
  continue;
@@ -9369,7 +9226,7 @@ var check28 = {
9369
9226
  registerCheck(check28);
9370
9227
 
9371
9228
  // src/checks/source/characters.ts
9372
- import { readFile as readFile24 } from "node:fs/promises";
9229
+ import { readFile as readFile23 } from "node:fs/promises";
9373
9230
 
9374
9231
  // src/checks/source/character-table.ts
9375
9232
  var CHARACTER_PATTERN = new RegExp("[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F\\u007F-\\u00A0\\u00AD\\u00B4\\u02BC" + "\\u061C\\u2000-\\u2015\\u2018\\u2019\\u201B-\\u201D\\u201F\\u2022\\u2026" + "\\u2028-\\u202F\\u2032\\u2033\\u205F\\u2060\\u2066-\\u2069\\u2190\\u2192" + "\\u2194\\u21D2\\u2212\\u25AA\\u25E6\\u2605\\u2713\\u2714\\u2717\\u3000" + "\\uE000-\\uF8FF\\uFEFF\\uFF02\\uFF07\\uFFFD]" + "|\\p{Emoji_Presentation}|\\p{Extended_Pictographic}(?=\\uFE0F)", "gu");
@@ -9492,7 +9349,7 @@ var check29 = {
9492
9349
  for (const file of (ctx.markdownFiles ?? []).slice().sort()) {
9493
9350
  let content;
9494
9351
  try {
9495
- content = await readFile24(file, "utf8");
9352
+ content = await readFile23(file, "utf8");
9496
9353
  } catch {
9497
9354
  results.push(inspectionFailed(check29.id, `Could not read source file: ${file}`, { file }));
9498
9355
  continue;
@@ -9532,16 +9389,16 @@ var check29 = {
9532
9389
  registerCheck(check29);
9533
9390
 
9534
9391
  // src/checks/asset/image-file-size.ts
9535
- import { stat as stat4 } from "node:fs/promises";
9392
+ import { stat as stat3 } from "node:fs/promises";
9536
9393
 
9537
9394
  // src/lib/image-inspect.ts
9538
- import { stat as stat3, readFile as readFile25 } from "node:fs/promises";
9395
+ import { stat as stat2, readFile as readFile24 } from "node:fs/promises";
9539
9396
  var DEFAULT_DPI = 72;
9540
9397
  var cache = new Map;
9541
9398
  async function inspectImage(path13) {
9542
9399
  let st;
9543
9400
  try {
9544
- st = await stat3(path13);
9401
+ st = await stat2(path13);
9545
9402
  } catch {
9546
9403
  return null;
9547
9404
  }
@@ -9550,7 +9407,7 @@ async function inspectImage(path13) {
9550
9407
  return hit.info;
9551
9408
  let info3 = null;
9552
9409
  try {
9553
- const buf = await readFile25(path13);
9410
+ const buf = await readFile24(path13);
9554
9411
  info3 = parseImage(buf);
9555
9412
  } catch {
9556
9413
  info3 = null;
@@ -9772,7 +9629,7 @@ var check30 = {
9772
9629
  const results = [];
9773
9630
  for (const file of files) {
9774
9631
  try {
9775
- const info3 = await stat4(file);
9632
+ const info3 = await stat3(file);
9776
9633
  if (info3.size > maxSize) {
9777
9634
  const sizeMb = (info3.size / 1e6).toFixed(1);
9778
9635
  const maxMb = (maxSize / 1e6).toFixed(1);
@@ -10110,7 +9967,7 @@ var check37 = {
10110
9967
  registerCheck(check37);
10111
9968
 
10112
9969
  // src/checks/heuristic/section-density.ts
10113
- import { readFile as readFile26 } from "node:fs/promises";
9970
+ import { readFile as readFile25 } from "node:fs/promises";
10114
9971
  var check38 = {
10115
9972
  id: "heuristic.chunking.section-density",
10116
9973
  name: "Section Density",
@@ -10127,7 +9984,7 @@ var check38 = {
10127
9984
  const results = [];
10128
9985
  for (const file of files) {
10129
9986
  try {
10130
- const content = await readFile26(file, "utf8");
9987
+ const content = await readFile25(file, "utf8");
10131
9988
  const lines = content.split(`
10132
9989
  `);
10133
9990
  let currentSectionStart = 0;
@@ -10857,6 +10714,7 @@ async function renderBook(ctx) {
10857
10714
  const htmlFile = await renderChaptersToFile(renderDir, workDir, {
10858
10715
  title: config.title,
10859
10716
  print: config.print,
10717
+ printArea: { ...config.page, preset: config.preset, spec: config.spec },
10860
10718
  styles: config.styles,
10861
10719
  files: config.source.files,
10862
10720
  plugins,
@@ -11009,7 +10867,7 @@ class PdfOutput {
11009
10867
  const rawPdf = pdfxMode ? path13.join(stage, "raw.pdf") : path13.resolve(pdfFile);
11010
10868
  await fsp2.mkdir(path13.dirname(path13.resolve(pdfFile)), { recursive: true });
11011
10869
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
11012
- const { buildNativePdf } = await import("./engine-yax79k9m.js");
10870
+ const { buildNativePdf } = await import("./engine-7ha27n2b.js");
11013
10871
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
11014
10872
  title: config.title,
11015
10873
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
@@ -11031,12 +10889,40 @@ class PdfOutput {
11031
10889
  if (pdfxMode) {
11032
10890
  const icc = opts.iccPath ?? config.pdfx.icc;
11033
10891
  effectiveIccPath = await resolveIccProfile(icc, manifestDir, opts.iccPath);
11034
- if (await hasLiveTransparency(await fsp2.readFile(rawPdf))) {
11035
- log.warn("This book uses transparency — an image with an alpha channel, or a CSS " + `rule like \`opacity\` or \`mix-blend-mode\`. PDF/${pdfxMode === "x1a" ? "X-1a" : "X-3"} has no way to represent that (both are based on PDF 1.3, which predates PDF transparency), so Ghostscript will flatten every page that uses it into a single raster ` + "image — that page loses its embedded fonts and searchable text in the " + "PDF/X output. To keep vector text, remove the `opacity`/blend rule and flatten any alpha-channel image against its intended background before including it (export it with no alpha channel), or build --format pdf instead of pdfx if this book doesn't need print-ready CMYK. `pdfx.rasterDpi` sets the resolution of those page images (default 720).");
10892
+ const rawBytes = await fsp2.readFile(rawPdf);
10893
+ const transparent = await transparentPages(rawBytes);
10894
+ const blankGradientPages = await findPostScriptShadingPages(rawBytes);
10895
+ const flattenedGradientPages = blankGradientPages.filter((n) => transparent.includes(n));
10896
+ const vectorGradientPages = blankGradientPages.filter((n) => !transparent.includes(n));
10897
+ const GRADIENT_FIX = "Replace it with a plain linear-gradient() or radial-gradient(), or a small tiled image. For 0deg/90deg stripes, `linear-gradient(90deg, #b33 0 6px, #c8c5bf 6px 10px)` with `background-size: 10px 100%` converts correctly.";
10898
+ const pageList = (pages) => `Page${pages.length === 1 ? "" : "s"} ${pages.join(", ")}`;
10899
+ if (vectorGradientPages.length > 0) {
10900
+ log.warn(`${pageList(vectorGradientPages)}: a repeating-*-gradient() or conic-gradient() ` + "can print blank or distorted — " + `Ghostscript has been seen to paint nothing, or irregular stripes, for it when converting to PDF/X; check the page visually. ${GRADIENT_FIX}`);
11036
10901
  }
11037
- const blankGradientPages = await findPostScriptShadingPages(await fsp2.readFile(rawPdf));
11038
- if (blankGradientPages.length > 0) {
11039
- log.warn(`Page${blankGradientPages.length === 1 ? "" : "s"} ${blankGradientPages.join(", ")}: ` + "a repeating-*-gradient() or conic-gradient() will print blank — " + "Ghostscript cannot convert it to PDF/X. Replace it with a plain linear-gradient() or radial-gradient(), or a small tiled image. For 0deg/90deg stripes, `linear-gradient(90deg, #b33 0 6px, #c8c5bf 6px 10px)` with `background-size: 10px 100%` converts correctly.");
10902
+ if (flattenedGradientPages.length > 0 && config.pdfx.transparency !== "preserve-text") {
10903
+ log.warn(`${pageList(flattenedGradientPages)}: a repeating-*-gradient() or conic-gradient() is painted only as part of the one raster image Ghostscript makes of the whole page, so the page loses its embedded fonts and searchable text. Check the page visually. ${GRADIENT_FIX}`);
10904
+ }
10905
+ const flattenPages = config.pdfx.flattenPages;
10906
+ if (transparent.length > 0 && config.pdfx.transparency === "preserve-text") {
10907
+ const { unsplittableTextReasons } = await import("./pdf-inspect-zdmy7qca.js");
10908
+ const reasons = await unsplittableTextReasons(rawBytes, transparent).catch((err) => {
10909
+ log.warn(`pdfx.transparency: preserve-text could not inspect this PDF's text (${err instanceof Error ? err.message : String(err)}), so every page that uses transparency is flattened whole, as with flatten-page.`);
10910
+ return null;
10911
+ });
10912
+ if (reasons) {
10913
+ const plan = planTransparentPages(transparent, reasons, flattenPages, blankGradientPages);
10914
+ await flattenTransparentPages(rawPdf, plan.split, stage, config.pdfx.rasterDpi);
10915
+ reportTransparentPlan(plan, pdfxMode, config.pdfx.rasterDpi ?? 300, pageList);
10916
+ }
10917
+ }
10918
+ const unused = flattenPages.filter((n) => !transparent.includes(n));
10919
+ if (flattenPages.length > 0 && config.pdfx.transparency !== "preserve-text") {
10920
+ log.warn("pdfx.flattenPages has no effect without pdfx.transparency: preserve-text.");
10921
+ } else if (unused.length > 0) {
10922
+ log.warn(`pdfx.flattenPages lists ${pageList(unused).toLowerCase()}, which ${unused.length === 1 ? "has" : "have"} no transparency (or do not exist): nothing to flatten there.`);
10923
+ }
10924
+ if (transparent.length > 0 && config.pdfx.transparency !== "preserve-text") {
10925
+ log.warn("This book uses transparency — an image with an alpha channel, or a CSS " + `rule like \`opacity\` or \`mix-blend-mode\`. PDF/${pdfxMode === "x1a" ? "X-1a" : "X-3"} has no way to represent that (both are based on PDF 1.3, which predates PDF transparency), so Ghostscript will flatten every page that uses it into a single raster ` + "image — that page loses its embedded fonts and searchable text in the " + "PDF/X output. To keep vector text, remove the `opacity`/blend rule and flatten any alpha-channel image against its intended background before including it (export it with no alpha channel), or build --format pdf instead of pdfx if this book doesn't need print-ready CMYK. Or set `pdfx.transparency: preserve-text` to keep the text live on the pages where that is possible. `pdfx.rasterDpi` sets the resolution of those page images (default 720).");
11040
10926
  }
11041
10927
  const shouldStrip = opts.stripAnnotations ?? config.pdfx.stripAnnotations;
11042
10928
  shouldStripAnnotations = shouldStrip;
@@ -11079,7 +10965,10 @@ class PdfOutput {
11079
10965
  resolvedFlavor: config.pdfx.flavor,
11080
10966
  iccPath: effectiveIccPath,
11081
10967
  stripAnnotations: shouldStripAnnotations,
11082
- blackText: pdfxMode ? config.pdfx.blackText : null
10968
+ blackText: pdfxMode ? config.pdfx.blackText : null,
10969
+ transparency: pdfxMode ? config.pdfx.transparency : null,
10970
+ flattenPages: pdfxMode ? config.pdfx.flattenPages : null,
10971
+ rasterDpi: pdfxMode ? config.pdfx.rasterDpi ?? null : null
11083
10972
  }
11084
10973
  }, `Wrote: ${reportedPdf}`, { htmlPath: htmlFile, pdfPath: reportedPdf }, pdfName, engineDiagnostics);
11085
10974
  } finally {
@@ -11116,11 +11005,40 @@ async function runBuild(opts) {
11116
11005
  await fsp2.rm(ctx.workDir, { recursive: true, force: true });
11117
11006
  }
11118
11007
  }
11008
+ var FALLBACK_CAUSE = {
11009
+ listed: "listed in pdfx.flattenPages",
11010
+ "text-transparency": "text drawn with transparency or blending (opacity, mix-blend-mode, a soft mask, a translucent text-shadow); keep that text opaque",
11011
+ "type3-font": "text in a Type 3 font: for a synthetic bold (a weight the font has no face for), use a face with that weight or set the font's own weight; for -webkit-text-stroke, remove the stroke or accept this fallback",
11012
+ "text-pattern-or-clip": 'text filled with a pattern or used as a clip (SVG fill="url(#…)", gradient text, background-clip: text)',
11013
+ gradient: "a repeating or conic gradient; use a linear or radial gradient, or a small tiled image"
11014
+ };
11015
+ function reportTransparentPlan({ split, artOnly, kept }, pdfxMode, dpi, pageList) {
11016
+ const one = (pages, singular, plural) => pages.length === 1 ? singular : plural;
11017
+ if (split.length > 0) {
11018
+ log.warn(`${pageList(split)} ${one(split, "uses", "use")} transparency, which PDF/${pdfxMode === "x1a" ? "X-1a" : "X-3"} (PDF 1.3) cannot represent. Each was flattened to a ${dpi} DPI picture with its text kept live on top (pdfx.transparency: preserve-text), so fonts and searchable text survive. On those pages, rules, backgrounds and shapes are raster, and text always sits above the artwork: text another element covers shows through. List a page in pdfx.flattenPages to flatten it whole instead.`);
11019
+ }
11020
+ if (artOnly.length > 0) {
11021
+ log.info(`${pageList(artOnly)} ${one(artOnly, "uses", "use")} transparency but ${one(artOnly, "shows", "show")} no text, so there is no text to keep: Ghostscript flattens ${one(artOnly, "it", "each")} whole (resolution: pdfx.rasterDpi).`);
11022
+ }
11023
+ if (kept.size > 0) {
11024
+ const pages = [...kept.keys()];
11025
+ const byCause = new Map;
11026
+ for (const [n, cause] of kept)
11027
+ byCause.set(cause, [...byCause.get(cause) ?? [], n]);
11028
+ const lines = [...byCause].map(([cause, ns]) => ` - ${one(ns, "page", "pages")} ${ns.join(", ")}: ${FALLBACK_CAUSE[cause]}`);
11029
+ log.warn(`${pageList(pages)} could not keep ${one(pages, "its", "their")} text live; Ghostscript will flatten ${one(pages, "that page", "each of those pages")} into one raster image, losing its embedded fonts and searchable text. Each page lists the first cause found; fixing it can reveal another:
11030
+ ` + lines.join(`
11031
+ `));
11032
+ }
11033
+ if (split.length === 0 && [...kept.values()].some((cause) => cause !== "listed")) {
11034
+ log.warn("pdfx.transparency: preserve-text kept no page's text live in this book: every page with transparency and text falls back (see above).");
11035
+ }
11036
+ }
11119
11037
  // src/preview/lifecycle.ts
11120
11038
  import path16 from "path";
11121
11039
  import { tmpdir } from "os";
11122
11040
  import { randomBytes as randomBytes2 } from "crypto";
11123
- import { readdir as readdir5, readFile as readFile27, writeFile as writeFile6 } from "node:fs/promises";
11041
+ import { readdir as readdir5, readFile as readFile26, writeFile as writeFile6 } from "node:fs/promises";
11124
11042
 
11125
11043
  // src/utils/file-utils.ts
11126
11044
  import { promises as fs3 } from "fs";
@@ -11223,6 +11141,7 @@ async function renderPreviewBook(inputPath, config, opts) {
11223
11141
  return renderChapters(inputPath, {
11224
11142
  title: config.title ?? "Document",
11225
11143
  print: config.print,
11144
+ printArea: config.printArea,
11226
11145
  styles: config.styles,
11227
11146
  files: opts.files,
11228
11147
  plugins,
@@ -11237,19 +11156,8 @@ async function renderPreviewBook(inputPath, config, opts) {
11237
11156
  }
11238
11157
  });
11239
11158
  }
11240
- function injectPreviewScripts(html, page) {
11241
- const area = page ? JSON.stringify({
11242
- width: page.width,
11243
- height: page.height,
11244
- bleed: page.bleed ?? 0,
11245
- safe: page.safe ?? 0,
11246
- artSafe: page.artSafe,
11247
- preset: page.preset,
11248
- spec: page.spec
11249
- }) : "";
11250
- const meta = area ? ` <meta name="gp-print-area" content='${area.replace(/&/g, "&amp;").replace(/'/g, "&#39;")}'>
11251
- ` : "";
11252
- const scripts = meta + ` <script src="/engine/gutterpress-viewer.js"></script>
11159
+ function injectPreviewScripts(html) {
11160
+ const scripts = ` <script src="/engine/gutterpress-viewer.js"></script>
11253
11161
  ` + `<script src="/preview/scripts/preview-interface.js"></script>
11254
11162
  ` + `<script src="/preview/scripts/preview-bridge.js"></script>
11255
11163
  `;
@@ -11264,7 +11172,10 @@ async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets, image
11264
11172
  }
11265
11173
  const nextAssets = new Map;
11266
11174
  const nextImageRefs = new Set;
11267
- const html = await renderPreviewBook(inputPath, config, {
11175
+ const html = await renderPreviewBook(inputPath, {
11176
+ ...config,
11177
+ printArea: config.page && { ...config.page, preset: config.preset, spec: config.spec }
11178
+ }, {
11268
11179
  files: config.source?.files ?? null,
11269
11180
  onCssAssets: (copies) => {
11270
11181
  for (const copy of copies)
@@ -11283,7 +11194,7 @@ async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets, image
11283
11194
  for (const ref of nextImageRefs)
11284
11195
  imageRefs.add(ref);
11285
11196
  }
11286
- await fsp3.writeFile(path15.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html, config.page && { ...config.page, preset: config.preset, spec: config.spec }), "utf-8");
11197
+ await fsp3.writeFile(path15.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html), "utf-8");
11287
11198
  }
11288
11199
  function isDotPathUnderRoot(candidatePath, root) {
11289
11200
  const normalizedRoot = root.replace(/\\/g, "/").replace(/\/+$/, "");
@@ -11681,7 +11592,7 @@ async function cleanupOrphanTempDirs() {
11681
11592
  const dir = path16.join(TEMP_DIR_BASE, entry);
11682
11593
  const pidFile = path16.join(dir, PID_FILE_NAME);
11683
11594
  try {
11684
- const raw = await readFile27(pidFile, "utf8");
11595
+ const raw = await readFile26(pidFile, "utf8");
11685
11596
  const pid = parseInt(raw.trim(), 10);
11686
11597
  if (!isProcessAlive(pid)) {
11687
11598
  debug(`Removing orphan temp dir ${dir} (pid ${pid} not alive)`);
@@ -11788,7 +11699,7 @@ function createServerState(inputPath, tempDir, config, options) {
11788
11699
  import http from "node:http";
11789
11700
  import net from "node:net";
11790
11701
  import { createHash as createHash5, randomUUID as randomUUID3 } from "node:crypto";
11791
- import { readFile as readFile28, stat as stat5 } from "node:fs/promises";
11702
+ import { readFile as readFile27, stat as stat4 } from "node:fs/promises";
11792
11703
  import path17 from "path";
11793
11704
  import { WebSocket, WebSocketServer } from "ws";
11794
11705
 
@@ -11877,7 +11788,7 @@ async function findAvailablePort(startPort, host = "127.0.0.1") {
11877
11788
  async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders = {}) {
11878
11789
  let filePath = absPath;
11879
11790
  try {
11880
- const s = await stat5(filePath);
11791
+ const s = await stat4(filePath);
11881
11792
  if (s.isDirectory())
11882
11793
  filePath = path17.join(filePath, "index.html");
11883
11794
  } catch {
@@ -11887,7 +11798,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
11887
11798
  }
11888
11799
  let data;
11889
11800
  try {
11890
- data = await readFile28(filePath);
11801
+ data = await readFile27(filePath);
11891
11802
  } catch {
11892
11803
  res.writeHead(404);
11893
11804
  res.end("Not Found");
@@ -11916,7 +11827,7 @@ async function serveRevalidatedStatic(req, absPath, res, validators) {
11916
11827
  }
11917
11828
  let fileStat;
11918
11829
  try {
11919
- fileStat = await stat5(absPath, { bigint: true });
11830
+ fileStat = await stat4(absPath, { bigint: true });
11920
11831
  } catch {
11921
11832
  await serveStatic(absPath, res);
11922
11833
  return;
@@ -11936,7 +11847,7 @@ async function serveRevalidatedStatic(req, absPath, res, validators) {
11936
11847
  let data;
11937
11848
  if (!validator || validator.stamp !== stamp) {
11938
11849
  try {
11939
- data = await readFile28(absPath);
11850
+ data = await readFile27(absPath);
11940
11851
  } catch {
11941
11852
  await serveStatic(absPath, res);
11942
11853
  return;
@@ -11964,7 +11875,7 @@ async function isMissingReferencedImage(pathname, absPath, refs) {
11964
11875
  if (!refs.has(imageRefKey(pathname)))
11965
11876
  return false;
11966
11877
  try {
11967
- await stat5(absPath);
11878
+ await stat4(absPath);
11968
11879
  return false;
11969
11880
  } catch (err) {
11970
11881
  return err?.code === "ENOENT";
@@ -12248,7 +12159,7 @@ async function startPreviewServer(options) {
12248
12159
  import { platform, arch, release } from "node:os";
12249
12160
 
12250
12161
  // src/lib/remote-auth/token-store.ts
12251
- import { chmod, mkdir as mkdir7, readFile as readFile29, rename as rename4, writeFile as writeFile7 } from "node:fs/promises";
12162
+ import { chmod, mkdir as mkdir7, readFile as readFile28, rename as rename4, writeFile as writeFile7 } from "node:fs/promises";
12252
12163
  import os3 from "node:os";
12253
12164
  import path18 from "node:path";
12254
12165
  function credentialHostKey(hostOrUrl) {
@@ -12292,7 +12203,7 @@ class FileTokenStore {
12292
12203
  async read() {
12293
12204
  let raw;
12294
12205
  try {
12295
- raw = await readFile29(this.filePath, "utf8");
12206
+ raw = await readFile28(this.filePath, "utf8");
12296
12207
  } catch (e) {
12297
12208
  if (e?.code === "ENOENT") {
12298
12209
  return { version: 1, credentials: {} };
@@ -12492,13 +12403,13 @@ async function getSystemDiagnostics() {
12492
12403
  };
12493
12404
  }
12494
12405
  // src/lib/project-scaffold.ts
12495
- import { access, copyFile, cp as cp2, mkdir as mkdir9, readdir as readdir6, readFile as readFile31, stat as stat6, writeFile as writeFile9 } from "node:fs/promises";
12406
+ import { access, copyFile, cp as cp2, mkdir as mkdir9, readdir as readdir6, readFile as readFile30, stat as stat5, writeFile as writeFile9 } from "node:fs/promises";
12496
12407
  import { constants as FS, existsSync as existsSync14 } from "node:fs";
12497
12408
  import path20 from "node:path";
12498
12409
 
12499
12410
  // src/lib/page-size.ts
12500
12411
  import { existsSync as existsSync13 } from "node:fs";
12501
- import { mkdir as mkdir8, readFile as readFile30, writeFile as writeFile8 } from "node:fs/promises";
12412
+ import { mkdir as mkdir8, readFile as readFile29, writeFile as writeFile8 } from "node:fs/promises";
12502
12413
  import path19 from "node:path";
12503
12414
  import postcss5 from "postcss";
12504
12415
  import { Scalar as Scalar2 } from "yaml";
@@ -12603,7 +12514,7 @@ var relDisplay2 = (projectDir, abs) => path19.relative(projectDir, abs).split(pa
12603
12514
  async function findCssSize(projectDir, sheets) {
12604
12515
  let found = null;
12605
12516
  for (const file of sheets) {
12606
- const size = readPageSizeFromCss(await readFile30(file, "utf8"));
12517
+ const size = readPageSizeFromCss(await readFile29(file, "utf8"));
12607
12518
  if (size)
12608
12519
  found = { file, size };
12609
12520
  }
@@ -12630,7 +12541,7 @@ async function setPageSetup(projectDir, choice) {
12630
12541
  const found = await findCssSize(projectDir, sheets);
12631
12542
  const newSheet = path19.join(projectDir, "styles", "book.css");
12632
12543
  const target = found?.file ?? sheets[sheets.length - 1] ?? newSheet;
12633
- const before = existsSync13(target) ? await readFile30(target, "utf8") : "";
12544
+ const before = existsSync13(target) ? await readFile29(target, "utf8") : "";
12634
12545
  let after;
12635
12546
  try {
12636
12547
  after = setPageSizeInCss(before, size);
@@ -12839,7 +12750,7 @@ async function adoptFolder(options) {
12839
12750
  }
12840
12751
  let st;
12841
12752
  try {
12842
- st = await stat6(dir);
12753
+ st = await stat5(dir);
12843
12754
  } catch {
12844
12755
  throw new CreateProjectErrorImpl("scaffold-io", `Folder not found: ${dir}`);
12845
12756
  }
@@ -12920,7 +12831,7 @@ styles:
12920
12831
  async function firstSourceFile(manifestPath) {
12921
12832
  let text;
12922
12833
  try {
12923
- text = await readFile31(manifestPath, "utf8");
12834
+ text = await readFile30(manifestPath, "utf8");
12924
12835
  } catch {
12925
12836
  return;
12926
12837
  }
@@ -12932,14 +12843,14 @@ async function firstSourceFile(manifestPath) {
12932
12843
  return m ? m[1].trim() : undefined;
12933
12844
  }
12934
12845
  async function fillTemplateFile(filePath, substitutions) {
12935
- let text = await readFile31(filePath, "utf8");
12846
+ let text = await readFile30(filePath, "utf8");
12936
12847
  for (const [token, value] of Object.entries(substitutions)) {
12937
12848
  text = text.split(token).join(value);
12938
12849
  }
12939
12850
  await writeFile9(filePath, text, "utf8");
12940
12851
  }
12941
12852
  // src/lib/extension-scaffold.ts
12942
- import { access as access2, mkdir as mkdir10, readFile as readFile32, writeFile as writeFile10 } from "node:fs/promises";
12853
+ import { access as access2, mkdir as mkdir10, readFile as readFile31, writeFile as writeFile10 } from "node:fs/promises";
12943
12854
  import { constants as FS2 } from "node:fs";
12944
12855
  import path21 from "node:path";
12945
12856
  var EXTENSION_KINDS = ["plugin", "theme"];
@@ -13056,7 +12967,7 @@ async function scaffoldExtension(options) {
13056
12967
  const src = await getAssetPath(`extension-templates/${kind}/${assetName}`);
13057
12968
  const dest = path21.join(extensionDir, relDest);
13058
12969
  await mkdir10(path21.dirname(dest), { recursive: true });
13059
- const text = await readFile32(src, "utf8");
12970
+ const text = await readFile31(src, "utf8");
13060
12971
  const table = relDest.endsWith(".json") ? jsonSafe : plain;
13061
12972
  await writeFile10(dest, substitute(text, table), "utf8");
13062
12973
  written.push(relDest);
@@ -13082,7 +12993,7 @@ function substitute(text, table) {
13082
12993
  return out;
13083
12994
  }
13084
12995
  // src/lib/project-templates.ts
13085
- import { cp as cp3, mkdir as mkdir11, readFile as readFile33, readdir as readdir7, stat as stat7, writeFile as writeFile11 } from "node:fs/promises";
12996
+ import { cp as cp3, mkdir as mkdir11, readFile as readFile32, readdir as readdir7, stat as stat6, writeFile as writeFile11 } from "node:fs/promises";
13086
12997
  import { constants as FS3, existsSync as existsSync15 } from "node:fs";
13087
12998
  import { access as access3 } from "node:fs/promises";
13088
12999
  import path22 from "node:path";
@@ -13094,7 +13005,7 @@ var BUILT_IN_TEMPLATE_IDS = [
13094
13005
  ];
13095
13006
  async function readTemplateChoices(manifestPath) {
13096
13007
  try {
13097
- const text = await readFile33(manifestPath, "utf8");
13008
+ const text = await readFile32(manifestPath, "utf8");
13098
13009
  const doc = parseDocument2(text);
13099
13010
  const out = {};
13100
13011
  const preset = doc.get("preset");
@@ -13276,7 +13187,7 @@ async function saveProjectAsTemplate(options) {
13276
13187
  async function retokeniseManifest(manifestPath) {
13277
13188
  let text;
13278
13189
  try {
13279
- text = await readFile33(manifestPath, "utf8");
13190
+ text = await readFile32(manifestPath, "utf8");
13280
13191
  } catch {
13281
13192
  return;
13282
13193
  }
@@ -13297,7 +13208,7 @@ async function listCustomTemplates(templatesRoot) {
13297
13208
  const dir = path22.join(templatesRoot, entry.name);
13298
13209
  let label = prettify(entry.name);
13299
13210
  try {
13300
- const meta = JSON.parse(await readFile33(path22.join(dir, TEMPLATE_META_FILE), "utf8"));
13211
+ const meta = JSON.parse(await readFile32(path22.join(dir, TEMPLATE_META_FILE), "utf8"));
13301
13212
  if (typeof meta.label === "string" && meta.label)
13302
13213
  label = meta.label;
13303
13214
  } catch {}
@@ -13315,7 +13226,7 @@ async function listCustomTemplates(templatesRoot) {
13315
13226
  }
13316
13227
  async function importTemplateFromFolder(options) {
13317
13228
  const { sourceDir, templatesRoot } = options;
13318
- const srcStat = await stat7(sourceDir);
13229
+ const srcStat = await stat6(sourceDir);
13319
13230
  if (!srcStat.isDirectory()) {
13320
13231
  throw new Error("The chosen path is not a folder.");
13321
13232
  }
@@ -13323,7 +13234,7 @@ async function importTemplateFromFolder(options) {
13323
13234
  return saveProjectAsTemplate({ projectDir: sourceDir, name, templatesRoot });
13324
13235
  }
13325
13236
  // src/lib/snippets.ts
13326
- import { readdir as readdir8, readFile as readFile34, writeFile as writeFile12, mkdir as mkdir12, rm as rm4 } from "node:fs/promises";
13237
+ import { readdir as readdir8, readFile as readFile33, writeFile as writeFile12, mkdir as mkdir12, rm as rm4 } from "node:fs/promises";
13327
13238
  import path23 from "node:path";
13328
13239
  var SNIPPETS_DIR = "snippets";
13329
13240
  var LEVEL_RANK = { project: 0, extension: 1, core: 2 };
@@ -13364,7 +13275,7 @@ async function scanSnippetFiles(dir) {
13364
13275
  continue;
13365
13276
  let body = "";
13366
13277
  try {
13367
- body = await readFile34(path23.join(dir, fileName), "utf8");
13278
+ body = await readFile33(path23.join(dir, fileName), "utf8");
13368
13279
  } catch {
13369
13280
  continue;
13370
13281
  }
@@ -13453,7 +13364,7 @@ async function collectLibrary(projectDir) {
13453
13364
  listed.component = name;
13454
13365
  continue;
13455
13366
  }
13456
- const body = await readFile34(path23.join(entry.dir, rel), "utf8").catch(() => null);
13367
+ const body = await readFile33(path23.join(entry.dir, rel), "utf8").catch(() => null);
13457
13368
  if (body === null)
13458
13369
  continue;
13459
13370
  snippets2.push({
@@ -13522,7 +13433,7 @@ async function listLocationVariants(projectDir) {
13522
13433
  }
13523
13434
  // src/lib/extension-import.ts
13524
13435
  import { existsSync as existsSync16 } from "node:fs";
13525
- import { cp as cp4, mkdir as mkdir13, mkdtemp as mkdtemp2, readFile as readFile35, rm as rm5, writeFile as writeFile13 } from "node:fs/promises";
13436
+ import { cp as cp4, mkdir as mkdir13, mkdtemp as mkdtemp2, readFile as readFile34, rm as rm5, writeFile as writeFile13 } from "node:fs/promises";
13526
13437
  import { tmpdir as tmpdir2 } from "node:os";
13527
13438
  import path24 from "node:path";
13528
13439
  import { unzipSync } from "fflate";
@@ -13595,7 +13506,7 @@ async function finalizeExtensionImport(projectDir, sourceDir, relPaths, fallback
13595
13506
  if (!existsSync16(cssPath)) {
13596
13507
  throw new Error("A theme must contain a theme.css file.");
13597
13508
  }
13598
- const css = await readFile35(cssPath, "utf8");
13509
+ const css = await readFile34(cssPath, "utf8");
13599
13510
  const { reject, warnings: printFindings } = classifyThemeCssFindings(checkCss(css, "theme.css"));
13600
13511
  if (reject) {
13601
13512
  throw new Error(`theme.css could not be parsed — ${reject.message}`);
@@ -13623,7 +13534,7 @@ async function finalizeExtensionImport(projectDir, sourceDir, relPaths, fallback
13623
13534
  for (let i = 0;i < extraSheets.length; i++) {
13624
13535
  const rel = extraSheets[i];
13625
13536
  const sheetPath = extraSheetPaths[i];
13626
- const sheetCss = await readFile35(sheetPath, "utf8");
13537
+ const sheetCss = await readFile34(sheetPath, "utf8");
13627
13538
  const { reject: sheetReject, warnings: sheetFindings } = classifyThemeCssFindings(checkCss(sheetCss, rel));
13628
13539
  if (sheetReject) {
13629
13540
  throw new Error(`${rel} could not be parsed — ${sheetReject.message}`);
@@ -13656,7 +13567,7 @@ async function completeLandedManifest(dest, id, meta, metaExists) {
13656
13567
  let pkg = {};
13657
13568
  if (metaExists) {
13658
13569
  try {
13659
- const parsed = JSON.parse(await readFile35(file, "utf8"));
13570
+ const parsed = JSON.parse(await readFile34(file, "utf8"));
13660
13571
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
13661
13572
  pkg = parsed;
13662
13573
  }
@@ -13758,11 +13669,11 @@ async function importExtensionFromCssText(projectDir, css, name) {
13758
13669
  async function importExtensionFromFile(projectDir, filePath) {
13759
13670
  const ext = path24.extname(filePath).toLowerCase();
13760
13671
  if (ext === ".zip") {
13761
- const buf = await readFile35(filePath);
13672
+ const buf = await readFile34(filePath);
13762
13673
  return importExtensionFromZip(projectDir, new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
13763
13674
  }
13764
13675
  if (ext === ".css") {
13765
- const css = await readFile35(filePath, "utf8");
13676
+ const css = await readFile34(filePath, "utf8");
13766
13677
  return importExtensionFromCssText(projectDir, css, prettify(path24.basename(filePath, ".css")));
13767
13678
  }
13768
13679
  throw new Error("Choose a .zip extension package or a .css stylesheet.");
@@ -16143,7 +16054,7 @@ async function scorchedEarth(options) {
16143
16054
  }
16144
16055
  }
16145
16056
  // src/lib/publish/run-publish.ts
16146
- import { readdir as readdir12, stat as stat12 } from "node:fs/promises";
16057
+ import { readdir as readdir12, stat as stat11 } from "node:fs/promises";
16147
16058
  import path33 from "node:path";
16148
16059
 
16149
16060
  // src/lib/publish/command-runner.ts
@@ -16374,7 +16285,7 @@ var drivethrurpgProvider = {
16374
16285
  };
16375
16286
 
16376
16287
  // src/lib/publish/providers/gdrive.ts
16377
- import { mkdtemp as mkdtemp3, readFile as readFile36, readdir as readdir11, rm as rm9, stat as stat9, writeFile as writeFile16 } from "node:fs/promises";
16288
+ import { mkdtemp as mkdtemp3, readFile as readFile35, readdir as readdir11, rm as rm9, stat as stat8, writeFile as writeFile16 } from "node:fs/promises";
16378
16289
  import { tmpdir as tmpdir3 } from "node:os";
16379
16290
  import path30 from "node:path";
16380
16291
  import { Zip, ZipDeflate } from "fflate";
@@ -16766,12 +16677,12 @@ async function collectZipEntries(root, dir, out, onWarn) {
16766
16677
  await collectZipEntries(root, full, out, onWarn);
16767
16678
  } else if (entry.isFile()) {
16768
16679
  const rel = path30.relative(root, full).split(path30.sep).join("/");
16769
- out[rel] = await readFile36(full);
16680
+ out[rel] = await readFile35(full);
16770
16681
  } else if (entry.isSymbolicLink()) {
16771
- const resolved = await stat9(full).catch(() => null);
16682
+ const resolved = await stat8(full).catch(() => null);
16772
16683
  if (resolved?.isFile()) {
16773
16684
  const rel = path30.relative(root, full).split(path30.sep).join("/");
16774
- out[rel] = await readFile36(full);
16685
+ out[rel] = await readFile35(full);
16775
16686
  } else if (resolved?.isDirectory()) {
16776
16687
  await collectZipEntries(root, full, out, onWarn);
16777
16688
  } else {
@@ -16841,7 +16752,7 @@ var gdriveProvider = {
16841
16752
  cleanup: async () => {}
16842
16753
  };
16843
16754
  try {
16844
- const artifactStat = await stat9(source.filePath);
16755
+ const artifactStat = await stat8(source.filePath);
16845
16756
  const about = await driveAbout(fetchImpl, accessToken);
16846
16757
  if (about.quota.limitBytes != null && about.quota.freeBytes != null) {
16847
16758
  if (artifactStat.size > about.quota.freeBytes) {
@@ -16886,7 +16797,7 @@ var gdriveProvider = {
16886
16797
  };
16887
16798
 
16888
16799
  // src/lib/publish/butler.ts
16889
- import { chmod as chmod2, mkdir as mkdir19, stat as stat10, writeFile as writeFile17 } from "node:fs/promises";
16800
+ import { chmod as chmod2, mkdir as mkdir19, stat as stat9, writeFile as writeFile17 } from "node:fs/promises";
16890
16801
  import path31 from "node:path";
16891
16802
  import { unzipSync as unzipSync2 } from "fflate";
16892
16803
  var BUTLER_DOWNLOAD_TIMEOUT_MS = 300000;
@@ -16907,7 +16818,7 @@ function butlerBinaryName(platform2 = process.platform) {
16907
16818
  }
16908
16819
  async function fileExists2(p) {
16909
16820
  try {
16910
- return (await stat10(p)).isFile();
16821
+ return (await stat9(p)).isFile();
16911
16822
  } catch {
16912
16823
  return false;
16913
16824
  }
@@ -17150,7 +17061,7 @@ var kdpProvider = {
17150
17061
  };
17151
17062
 
17152
17063
  // src/lib/publish/providers/local-folder.ts
17153
- import { cp as cp7, mkdir as mkdir20, stat as stat11 } from "node:fs/promises";
17064
+ import { cp as cp7, mkdir as mkdir20, stat as stat10 } from "node:fs/promises";
17154
17065
  import path32 from "node:path";
17155
17066
  var LOCAL_PUBLISH_DEFAULT_DIR = "dist";
17156
17067
  var info8 = {
@@ -17177,7 +17088,7 @@ var localFolderProvider = {
17177
17088
  async preflight(req) {
17178
17089
  const dir = localPublishDir(req);
17179
17090
  try {
17180
- const s = await stat11(dir);
17091
+ const s = await stat10(dir);
17181
17092
  if (!s.isDirectory()) {
17182
17093
  return [
17183
17094
  { severity: "error", id: "local/not-a-folder", message: `${dir} exists but is not a folder.` }
@@ -17472,7 +17383,7 @@ var PDF_HINT = "Build the PDF first (gutterpress build, or export it from the ap
17472
17383
  var HTML_HINT = "Build the website export first (gutterpress build --format html).";
17473
17384
  async function artifactIssues(artifact) {
17474
17385
  try {
17475
- const s = await stat12(artifact.path);
17386
+ const s = await stat11(artifact.path);
17476
17387
  if (artifact.format === "pdf" && !s.isFile()) {
17477
17388
  return [
17478
17389
  {
@@ -17518,7 +17429,7 @@ async function artifactIssues(artifact) {
17518
17429
  var BUILD_FINGERPRINT_FILENAME = "build-fingerprint.json";
17519
17430
  async function htmlDirIssues(dir) {
17520
17431
  const issues = [];
17521
- const isThere = async (rel) => stat12(path33.join(dir, rel)).then(() => true, () => false);
17432
+ const isThere = async (rel) => stat11(path33.join(dir, rel)).then(() => true, () => false);
17522
17433
  if (!await isThere(BOOK_HTML)) {
17523
17434
  issues.push({
17524
17435
  severity: "error",
@@ -17637,7 +17548,7 @@ async function disconnectPublishCredential(key, deps, options = {}) {
17637
17548
  }
17638
17549
  }
17639
17550
  // src/lib/publish/selections.ts
17640
- import { chmod as chmod3, mkdir as mkdir21, readFile as readFile37, writeFile as writeFile18 } from "node:fs/promises";
17551
+ import { chmod as chmod3, mkdir as mkdir21, readFile as readFile36, writeFile as writeFile18 } from "node:fs/promises";
17641
17552
  import path34 from "node:path";
17642
17553
  class PublishSelectionsStore {
17643
17554
  filePath;
@@ -17647,7 +17558,7 @@ class PublishSelectionsStore {
17647
17558
  }
17648
17559
  async read() {
17649
17560
  try {
17650
- const raw = await readFile37(this.filePath, "utf8");
17561
+ const raw = await readFile36(this.filePath, "utf8");
17651
17562
  const parsed = JSON.parse(raw);
17652
17563
  if (parsed && typeof parsed === "object") {
17653
17564
  return {