@1agh/maude 1.0.11 → 1.2.0

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 (506) hide show
  1. package/apps/studio/acp/bridge.ts +18 -13
  2. package/apps/studio/acp/transcript-io.ts +394 -0
  3. package/apps/studio/acp/transcript.ts +103 -38
  4. package/apps/studio/api.ts +139 -26
  5. package/apps/studio/bin/_import-figma.mjs +57 -11
  6. package/apps/studio/bin/_seed-fixture.mjs +163 -0
  7. package/apps/studio/canvas-imports.ts +89 -0
  8. package/apps/studio/client/app.jsx +257 -7
  9. package/apps/studio/client/export-center.jsx +15 -7
  10. package/apps/studio/client/panels/SyncPanel.jsx +125 -4
  11. package/apps/studio/client/report-bug.jsx +19 -1
  12. package/apps/studio/client/styles/3-shell-maude.css +34 -0
  13. package/apps/studio/client/styles/4-components.css +26 -0
  14. package/apps/studio/collab/persistence.ts +132 -16
  15. package/apps/studio/collab/registry.ts +10 -18
  16. package/apps/studio/debug-bundle.ts +13 -0
  17. package/apps/studio/dist/client.bundle.js +1128 -1128
  18. package/apps/studio/dist/styles.css +1 -1
  19. package/apps/studio/export-dialog.tsx +54 -0
  20. package/apps/studio/exporters/degraded.ts +19 -3
  21. package/apps/studio/exporters/ghostscript.ts +194 -0
  22. package/apps/studio/exporters/pdf-fonts.ts +261 -0
  23. package/apps/studio/exporters/pdf.ts +152 -3
  24. package/apps/studio/exporters/remote.ts +36 -1
  25. package/apps/studio/fs-watch.ts +45 -1
  26. package/apps/studio/http.ts +7 -1
  27. package/apps/studio/mem.ts +45 -9
  28. package/apps/studio/server.ts +11 -4
  29. package/apps/studio/sync/agent.ts +136 -29
  30. package/apps/studio/sync/asset-push.ts +1 -47
  31. package/apps/studio/sync/codec.ts +122 -31
  32. package/apps/studio/sync/cold-start-apply.ts +7 -0
  33. package/apps/studio/sync/cold-start.ts +118 -22
  34. package/apps/studio/sync/comment-identity.ts +68 -0
  35. package/apps/studio/sync/connection-state.ts +15 -1
  36. package/apps/studio/sync/file-ledger.ts +117 -0
  37. package/apps/studio/sync/file-plane.ts +623 -27
  38. package/apps/studio/sync/index.ts +711 -20
  39. package/apps/studio/sync/journal-client.ts +13 -1
  40. package/apps/studio/sync/migrate-seed.ts +40 -3
  41. package/apps/studio/sync/presentation.ts +26 -3
  42. package/apps/studio/sync/retry-after.ts +169 -0
  43. package/apps/studio/sync/seed-progress.ts +218 -0
  44. package/apps/studio/sync/status.ts +122 -5
  45. package/apps/studio/sync/transport-error.ts +111 -0
  46. package/apps/studio/whats-new.json +72 -0
  47. package/cli/bin/maude.mjs +2 -0
  48. package/cli/commands/codex.mjs +191 -0
  49. package/cli/commands/harness.mjs +1051 -0
  50. package/cli/commands/help.mjs +13 -0
  51. package/cli/lib/design-link.mjs +190 -2
  52. package/cli/lib/harness/capabilities.mjs +302 -0
  53. package/cli/lib/harness/codex-runtime.mjs +889 -0
  54. package/cli/lib/harness/compatibility.mjs +27 -0
  55. package/cli/lib/harness/discover-claude.mjs +1393 -0
  56. package/cli/lib/harness/managed-state.mjs +1296 -0
  57. package/cli/lib/harness/model.mjs +86 -0
  58. package/cli/lib/harness/secrets.mjs +390 -0
  59. package/cli/lib/harness/targets/codex.mjs +1141 -0
  60. package/cli/lib/harness/targets/opencode.mjs +807 -0
  61. package/cli/lib/harness/transaction.mjs +1099 -0
  62. package/cli/lib/hubs-config.mjs +35 -8
  63. package/cli/templates/harness/opencode/maude-projector.ts +58 -0
  64. package/docs/harness-capability-matrix.md +125 -0
  65. package/docs/harness-environment-projection.md +95 -0
  66. package/package.json +32 -9
  67. package/plugins/flow/.claude-plugin/config.schema.json +8 -0
  68. package/apps/studio/.ai/cache/_stats.json +0 -7
  69. package/apps/studio/bin/_agent-browser-safe.test.mjs +0 -165
  70. package/apps/studio/bin/_curl-local.test.mjs +0 -280
  71. package/apps/studio/bin/_pw-launch.test.mjs +0 -90
  72. package/apps/studio/bin/_smart-frames.test.mjs +0 -195
  73. package/apps/studio/bin/_transcribe.test.mjs +0 -80
  74. package/apps/studio/bun.lock +0 -928
  75. package/apps/studio/draw/test/animate.test.ts +0 -172
  76. package/apps/studio/draw/test/brush.test.ts +0 -213
  77. package/apps/studio/draw/test/composition.test.ts +0 -141
  78. package/apps/studio/draw/test/geometry.test.ts +0 -199
  79. package/apps/studio/draw/test/gradient.test.ts +0 -167
  80. package/apps/studio/draw/test/layout.test.ts +0 -120
  81. package/apps/studio/draw/test/morph.test.ts +0 -107
  82. package/apps/studio/draw/test/optimize.test.ts +0 -40
  83. package/apps/studio/draw/test/palette.test.ts +0 -123
  84. package/apps/studio/draw/test/primitives.test.ts +0 -129
  85. package/apps/studio/draw/test/serialize-animate.test.ts +0 -142
  86. package/apps/studio/draw/test/serialize.test.ts +0 -105
  87. package/apps/studio/figma/assets.test.ts +0 -464
  88. package/apps/studio/figma/client.test.ts +0 -395
  89. package/apps/studio/figma/codegen-client.test.ts +0 -276
  90. package/apps/studio/figma/codegen-fonts.test.ts +0 -103
  91. package/apps/studio/figma/codegen-values.test.ts +0 -179
  92. package/apps/studio/figma/comments-to-strokes.test.ts +0 -194
  93. package/apps/studio/figma/fig-decode.test.ts +0 -788
  94. package/apps/studio/figma/fig-differential.test.ts +0 -182
  95. package/apps/studio/figma/fig-translator.test.ts +0 -192
  96. package/apps/studio/figma/fig-vector.test.ts +0 -113
  97. package/apps/studio/figma/from-codegen.test.ts +0 -408
  98. package/apps/studio/figma/sanitize.test.ts +0 -325
  99. package/apps/studio/figma/tailwind-map.test.ts +0 -142
  100. package/apps/studio/figma/to-artboard.test.ts +0 -808
  101. package/apps/studio/figma/to-render.test.ts +0 -180
  102. package/apps/studio/figma/to-strokes-roundtrip.test.ts +0 -152
  103. package/apps/studio/figma/to-strokes.test.ts +0 -705
  104. package/apps/studio/figma/to-tokens.test.ts +0 -321
  105. package/apps/studio/figma/url.test.ts +0 -167
  106. package/apps/studio/footage/schema.test.ts +0 -287
  107. package/apps/studio/generation/adapters/groq.test.ts +0 -80
  108. package/apps/studio/generation/audio-library.test.ts +0 -95
  109. package/apps/studio/generation/captions.test.ts +0 -78
  110. package/apps/studio/generation/elevenlabs.test.ts +0 -223
  111. package/apps/studio/generation/gemini.test.ts +0 -350
  112. package/apps/studio/generation/jobs.test.ts +0 -128
  113. package/apps/studio/generation/keys.test.ts +0 -75
  114. package/apps/studio/generation/prefs.test.ts +0 -95
  115. package/apps/studio/generation/types.test.ts +0 -123
  116. package/apps/studio/generation/whisper-models.test.ts +0 -179
  117. package/apps/studio/test/_helpers.ts +0 -108
  118. package/apps/studio/test/acp-activity-endpoint.test.ts +0 -183
  119. package/apps/studio/test/acp-activity.test.ts +0 -154
  120. package/apps/studio/test/acp-ai-activity.test.ts +0 -182
  121. package/apps/studio/test/acp-attachment-serve.test.ts +0 -74
  122. package/apps/studio/test/acp-bootstrap-brief.test.ts +0 -178
  123. package/apps/studio/test/acp-branch-guard.test.ts +0 -123
  124. package/apps/studio/test/acp-bridge-lifetime.test.ts +0 -475
  125. package/apps/studio/test/acp-bridge.test.ts +0 -545
  126. package/apps/studio/test/acp-capabilities.test.ts +0 -123
  127. package/apps/studio/test/acp-caps-bridge.test.ts +0 -279
  128. package/apps/studio/test/acp-commands.test.ts +0 -119
  129. package/apps/studio/test/acp-elicitation-bridge.test.ts +0 -480
  130. package/apps/studio/test/acp-elicitation.test.ts +0 -251
  131. package/apps/studio/test/acp-env.test.ts +0 -95
  132. package/apps/studio/test/acp-mode-banner.test.ts +0 -45
  133. package/apps/studio/test/acp-origin-gate.test.ts +0 -160
  134. package/apps/studio/test/acp-permission-prompt.test.ts +0 -251
  135. package/apps/studio/test/acp-permission.test.ts +0 -278
  136. package/apps/studio/test/acp-plugin-bootstrap.test.ts +0 -89
  137. package/apps/studio/test/acp-session-allowed-tools.test.ts +0 -235
  138. package/apps/studio/test/acp-session-plugins.test.ts +0 -209
  139. package/apps/studio/test/acp-toolgroup.test.ts +0 -76
  140. package/apps/studio/test/acp-transcript-view.test.ts +0 -71
  141. package/apps/studio/test/acp-transcript.test.ts +0 -238
  142. package/apps/studio/test/acp-usage-bridge.test.ts +0 -141
  143. package/apps/studio/test/acp-usage.test.ts +0 -143
  144. package/apps/studio/test/acp-write-gate.test.ts +0 -323
  145. package/apps/studio/test/acp-write-scope.test.ts +0 -533
  146. package/apps/studio/test/active-state.test.ts +0 -186
  147. package/apps/studio/test/activity.test.ts +0 -189
  148. package/apps/studio/test/ai-activity.test.ts +0 -113
  149. package/apps/studio/test/annotate-write.test.ts +0 -1074
  150. package/apps/studio/test/annotation-strokes-command.test.ts +0 -166
  151. package/apps/studio/test/annotations-align.test.ts +0 -88
  152. package/apps/studio/test/annotations-api.test.ts +0 -145
  153. package/apps/studio/test/annotations-bindings.test.ts +0 -262
  154. package/apps/studio/test/annotations-draw-modifiers.test.ts +0 -206
  155. package/apps/studio/test/annotations-groups.test.ts +0 -231
  156. package/apps/studio/test/annotations-layer.test.ts +0 -1011
  157. package/apps/studio/test/annotations-roundtrip.test.ts +0 -663
  158. package/apps/studio/test/annotations-snap.test.ts +0 -135
  159. package/apps/studio/test/annotations-text-format.test.ts +0 -396
  160. package/apps/studio/test/artboard-activity-overlay.test.tsx +0 -56
  161. package/apps/studio/test/artboard-guides-overlay.test.tsx +0 -152
  162. package/apps/studio/test/artboard-hug-commit.test.ts +0 -36
  163. package/apps/studio/test/artboard-kinds.test.tsx +0 -83
  164. package/apps/studio/test/artboard-selection-attrs.test.ts +0 -69
  165. package/apps/studio/test/asset-api.test.ts +0 -149
  166. package/apps/studio/test/assets-s3.test.ts +0 -249
  167. package/apps/studio/test/binary-smoke.test.ts +0 -47
  168. package/apps/studio/test/boot-self-heal.test.ts +0 -87
  169. package/apps/studio/test/browse-posture.test.tsx +0 -107
  170. package/apps/studio/test/bundle-smoke.test.ts +0 -46
  171. package/apps/studio/test/camera-reveal.test.tsx +0 -173
  172. package/apps/studio/test/canvas-artifacts.test.ts +0 -125
  173. package/apps/studio/test/canvas-build-sandbox.test.ts +0 -69
  174. package/apps/studio/test/canvas-build.test.ts +0 -141
  175. package/apps/studio/test/canvas-create-api.test.ts +0 -676
  176. package/apps/studio/test/canvas-cursors.test.ts +0 -164
  177. package/apps/studio/test/canvas-edit.test.ts +0 -470
  178. package/apps/studio/test/canvas-freshness-deps.test.ts +0 -64
  179. package/apps/studio/test/canvas-header.test.ts +0 -127
  180. package/apps/studio/test/canvas-hide-chrome.test.ts +0 -88
  181. package/apps/studio/test/canvas-hmr-runtime.test.tsx +0 -114
  182. package/apps/studio/test/canvas-lib-inline.test.ts +0 -146
  183. package/apps/studio/test/canvas-lib-motion.test.ts +0 -131
  184. package/apps/studio/test/canvas-lib-resolver.test.ts +0 -111
  185. package/apps/studio/test/canvas-list-watch.test.ts +0 -388
  186. package/apps/studio/test/canvas-media-drop.test.ts +0 -187
  187. package/apps/studio/test/canvas-meta-api.test.ts +0 -707
  188. package/apps/studio/test/canvas-move-api.test.ts +0 -587
  189. package/apps/studio/test/canvas-origin-gate.test.ts +0 -417
  190. package/apps/studio/test/canvas-pipeline.test.ts +0 -180
  191. package/apps/studio/test/canvas-rects.test.ts +0 -198
  192. package/apps/studio/test/canvas-reorder.test.ts +0 -211
  193. package/apps/studio/test/canvas-route.test.ts +0 -179
  194. package/apps/studio/test/canvas-shell-base.test.ts +0 -107
  195. package/apps/studio/test/canvas-shell-build-error.test.ts +0 -52
  196. package/apps/studio/test/canvas-url.test.ts +0 -138
  197. package/apps/studio/test/canvas-wheel-delta.test.ts +0 -42
  198. package/apps/studio/test/canvas-zoom-floor.test.ts +0 -73
  199. package/apps/studio/test/capture-core.test.ts +0 -32
  200. package/apps/studio/test/capture-determinism-shape.test.ts +0 -135
  201. package/apps/studio/test/chat-attachments.test.ts +0 -117
  202. package/apps/studio/test/chat-context.test.ts +0 -129
  203. package/apps/studio/test/chat-markdown.test.tsx +0 -58
  204. package/apps/studio/test/clip-addressing.test.ts +0 -738
  205. package/apps/studio/test/clip-ops.test.ts +0 -922
  206. package/apps/studio/test/cloud-build.test.ts +0 -95
  207. package/apps/studio/test/cloud-connect-note.test.ts +0 -135
  208. package/apps/studio/test/cloud-endpoints.test.ts +0 -590
  209. package/apps/studio/test/cloud-history-hardening.test.ts +0 -165
  210. package/apps/studio/test/cloud-history-posture.test.ts +0 -230
  211. package/apps/studio/test/cloud-managed-save-surfaces.test.ts +0 -90
  212. package/apps/studio/test/cloud-renew.test.ts +0 -205
  213. package/apps/studio/test/cloud-session-role.test.ts +0 -161
  214. package/apps/studio/test/cloud-shell-surfaces.test.ts +0 -170
  215. package/apps/studio/test/cold-start-apply.test.ts +0 -303
  216. package/apps/studio/test/collab-annotations-bridge.test.ts +0 -55
  217. package/apps/studio/test/collab-awareness-bridge.test.ts +0 -223
  218. package/apps/studio/test/collab-bridge.test.ts +0 -79
  219. package/apps/studio/test/collab-loopback.test.ts +0 -63
  220. package/apps/studio/test/collab-origin-gate.test.ts +0 -323
  221. package/apps/studio/test/collab-protocol.test.ts +0 -146
  222. package/apps/studio/test/collab-readonly-gate.test.ts +0 -194
  223. package/apps/studio/test/collab-reseed-guard.test.ts +0 -78
  224. package/apps/studio/test/collab-room.test.ts +0 -188
  225. package/apps/studio/test/collab-session-survival.test.tsx +0 -176
  226. package/apps/studio/test/collab-stress.test.ts +0 -129
  227. package/apps/studio/test/comment-mount.test.ts +0 -97
  228. package/apps/studio/test/comment-relay-origin-gate.test.ts +0 -117
  229. package/apps/studio/test/comments-api.test.ts +0 -277
  230. package/apps/studio/test/comments-fs-rebroadcast.test.ts +0 -155
  231. package/apps/studio/test/comments-overlay.test.ts +0 -117
  232. package/apps/studio/test/compile-entry.test.ts +0 -133
  233. package/apps/studio/test/component-map.test.ts +0 -48
  234. package/apps/studio/test/config-projection.test.ts +0 -104
  235. package/apps/studio/test/config-reload.test.ts +0 -230
  236. package/apps/studio/test/config-version.test.ts +0 -88
  237. package/apps/studio/test/context-resolve-tokens.test.ts +0 -97
  238. package/apps/studio/test/convert-to-absolute.test.ts +0 -333
  239. package/apps/studio/test/credential-grammar.test.ts +0 -107
  240. package/apps/studio/test/csp-canvas-shell.test.ts +0 -62
  241. package/apps/studio/test/csrf-write-guard.test.ts +0 -141
  242. package/apps/studio/test/debug-bundle.test.ts +0 -105
  243. package/apps/studio/test/design-setup-readiness.test.ts +0 -155
  244. package/apps/studio/test/detach-component.test.ts +0 -94
  245. package/apps/studio/test/dns-rebinding-guard.test.ts +0 -101
  246. package/apps/studio/test/dom-selection.test.ts +0 -198
  247. package/apps/studio/test/dynamic-text-edit.test.ts +0 -162
  248. package/apps/studio/test/edit-css-occurrence.test.ts +0 -81
  249. package/apps/studio/test/edit-persistence.test.ts +0 -91
  250. package/apps/studio/test/edit-scope-api.test.ts +0 -119
  251. package/apps/studio/test/edit-source-command.test.ts +0 -142
  252. package/apps/studio/test/edit-suppress.test.ts +0 -170
  253. package/apps/studio/test/editing-presence.test.ts +0 -103
  254. package/apps/studio/test/element-resize.test.ts +0 -136
  255. package/apps/studio/test/element-structural-api.test.ts +0 -434
  256. package/apps/studio/test/element-structural-edit.test.ts +0 -750
  257. package/apps/studio/test/ensure-browser.test.ts +0 -92
  258. package/apps/studio/test/equal-spacing-detector.test.ts +0 -257
  259. package/apps/studio/test/export-assemble.test.ts +0 -53
  260. package/apps/studio/test/export-browser-lane.test.ts +0 -126
  261. package/apps/studio/test/export-capture-fidelity.test.ts +0 -146
  262. package/apps/studio/test/export-capture-hygiene.test.ts +0 -675
  263. package/apps/studio/test/export-center.test.tsx +0 -287
  264. package/apps/studio/test/export-e2e-lanes.test.ts +0 -857
  265. package/apps/studio/test/export-format-scope-coherence.test.ts +0 -89
  266. package/apps/studio/test/export-lane.test.ts +0 -245
  267. package/apps/studio/test/export-shim-multi-capture.test.ts +0 -83
  268. package/apps/studio/test/exporters/canva.test.ts +0 -63
  269. package/apps/studio/test/exporters/degraded-propagation.test.ts +0 -123
  270. package/apps/studio/test/exporters/endpoint.test.ts +0 -121
  271. package/apps/studio/test/exporters/history.test.ts +0 -107
  272. package/apps/studio/test/exporters/html.test.ts +0 -26
  273. package/apps/studio/test/exporters/jobs.test.ts +0 -342
  274. package/apps/studio/test/exporters/pdf.test.ts +0 -85
  275. package/apps/studio/test/exporters/png.test.ts +0 -103
  276. package/apps/studio/test/exporters/pptx-deck.test.ts +0 -112
  277. package/apps/studio/test/exporters/pptx.test.ts +0 -31
  278. package/apps/studio/test/exporters/pw-launch.test.ts +0 -49
  279. package/apps/studio/test/exporters/runtime.test.ts +0 -59
  280. package/apps/studio/test/exporters/scope.test.ts +0 -292
  281. package/apps/studio/test/exporters/svg.test.ts +0 -29
  282. package/apps/studio/test/exporters/unsupported-media.test.ts +0 -123
  283. package/apps/studio/test/exporters/zip.test.ts +0 -104
  284. package/apps/studio/test/fetch-asset-gate.test.ts +0 -189
  285. package/apps/studio/test/figjam-v3-model.test.ts +0 -342
  286. package/apps/studio/test/figma-explode.test.ts +0 -438
  287. package/apps/studio/test/figma-provenance.test.ts +0 -108
  288. package/apps/studio/test/figma-routes.test.ts +0 -294
  289. package/apps/studio/test/file-lock.test.ts +0 -84
  290. package/apps/studio/test/fixtures/backgrounding-claude-auth.sh +0 -12
  291. package/apps/studio/test/fixtures/fake-claude-auth.mjs +0 -23
  292. package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +0 -1
  293. package/apps/studio/test/fixtures/mock-acp-agent-caps.mjs +0 -158
  294. package/apps/studio/test/fixtures/mock-acp-agent-commands.mjs +0 -44
  295. package/apps/studio/test/fixtures/mock-acp-agent-elicit-flood.mjs +0 -46
  296. package/apps/studio/test/fixtures/mock-acp-agent-elicit-url.mjs +0 -42
  297. package/apps/studio/test/fixtures/mock-acp-agent-elicit.mjs +0 -63
  298. package/apps/studio/test/fixtures/mock-acp-agent-permission-flood.mjs +0 -51
  299. package/apps/studio/test/fixtures/mock-acp-agent-permission.mjs +0 -50
  300. package/apps/studio/test/fixtures/mock-acp-agent-slow.mjs +0 -45
  301. package/apps/studio/test/fixtures/mock-acp-agent-usage.mjs +0 -56
  302. package/apps/studio/test/fixtures/mock-acp-agent-wedged.mjs +0 -37
  303. package/apps/studio/test/fixtures/mock-acp-agent-write.mjs +0 -118
  304. package/apps/studio/test/fixtures/mock-acp-agent.mjs +0 -69
  305. package/apps/studio/test/fixtures/noisy-claude-auth.mjs +0 -27
  306. package/apps/studio/test/fixtures/perf-canvas.mjs +0 -201
  307. package/apps/studio/test/fixtures/phase-20-annotations.svg +0 -1
  308. package/apps/studio/test/fixtures/phase-21-annotations.svg +0 -1
  309. package/apps/studio/test/fixtures/tiny-subset.woff2 +0 -0
  310. package/apps/studio/test/fixtures/video-comp-fixture.tsx +0 -82
  311. package/apps/studio/test/fixtures/wedged-claude-auth.sh +0 -11
  312. package/apps/studio/test/footage-store.test.ts +0 -100
  313. package/apps/studio/test/fs-mkdir-api.test.ts +0 -248
  314. package/apps/studio/test/fs-watch.test.ts +0 -41
  315. package/apps/studio/test/gemma-models.test.ts +0 -245
  316. package/apps/studio/test/generate-route.test.ts +0 -106
  317. package/apps/studio/test/git-api.test.ts +0 -0
  318. package/apps/studio/test/git-branches.test.ts +0 -341
  319. package/apps/studio/test/git-cloud-posture.test.ts +0 -59
  320. package/apps/studio/test/git-grouping.test.ts +0 -106
  321. package/apps/studio/test/git-lifecycle.test.ts +0 -100
  322. package/apps/studio/test/git-log-format.test.ts +0 -95
  323. package/apps/studio/test/git-safe-rel.test.ts +0 -132
  324. package/apps/studio/test/git-system-engine.test.ts +0 -135
  325. package/apps/studio/test/git-watch.test.ts +0 -97
  326. package/apps/studio/test/github-api.test.ts +0 -577
  327. package/apps/studio/test/grid-track-handles.test.ts +0 -160
  328. package/apps/studio/test/handoff-static-frames.test.ts +0 -215
  329. package/apps/studio/test/handoff.test.ts +0 -329
  330. package/apps/studio/test/history-rollback.test.ts +0 -88
  331. package/apps/studio/test/hmr-broadcast.test.ts +0 -236
  332. package/apps/studio/test/hmr-classify.test.ts +0 -87
  333. package/apps/studio/test/hub-link.test.ts +0 -80
  334. package/apps/studio/test/identity-cache.test.ts +0 -83
  335. package/apps/studio/test/import-asset-browser.test.ts +0 -64
  336. package/apps/studio/test/import-asset-route.test.ts +0 -71
  337. package/apps/studio/test/import-asset.test.ts +0 -427
  338. package/apps/studio/test/import-brand-browser.test.ts +0 -88
  339. package/apps/studio/test/import-brand-route.test.ts +0 -120
  340. package/apps/studio/test/import-brand.test.ts +0 -206
  341. package/apps/studio/test/import-figma.test.ts +0 -667
  342. package/apps/studio/test/import-tokens.test.ts +0 -722
  343. package/apps/studio/test/index-data-canvas-ds.test.ts +0 -97
  344. package/apps/studio/test/input-router.test.ts +0 -757
  345. package/apps/studio/test/inspect-script-syntax.test.ts +0 -53
  346. package/apps/studio/test/inspect-selections.test.ts +0 -219
  347. package/apps/studio/test/knob-props-authored.test.ts +0 -87
  348. package/apps/studio/test/layers-synthetic-groups.test.ts +0 -96
  349. package/apps/studio/test/locator.test.ts +0 -214
  350. package/apps/studio/test/marquee-overlay.test.ts +0 -94
  351. package/apps/studio/test/media-commit-chain.test.ts +0 -210
  352. package/apps/studio/test/mode-toggle.test.tsx +0 -226
  353. package/apps/studio/test/motion-sample.test.ts +0 -35
  354. package/apps/studio/test/move-artboards-command.test.ts +0 -108
  355. package/apps/studio/test/participants-chrome.test.ts +0 -65
  356. package/apps/studio/test/paths.test.ts +0 -88
  357. package/apps/studio/test/pdf-print-boxes.test.ts +0 -359
  358. package/apps/studio/test/peer-selection-follows-camera.test.tsx +0 -131
  359. package/apps/studio/test/perf-harness.ts +0 -193
  360. package/apps/studio/test/phase-3.6-smoke.test.ts +0 -79
  361. package/apps/studio/test/photo-bg-remove-validation.test.ts +0 -71
  362. package/apps/studio/test/photo-canvas-bundle.test.ts +0 -79
  363. package/apps/studio/test/photo-edit-api.test.ts +0 -201
  364. package/apps/studio/test/photo-filters.test.ts +0 -149
  365. package/apps/studio/test/photo-pipeline.test.ts +0 -106
  366. package/apps/studio/test/photo-taxonomy.test.ts +0 -96
  367. package/apps/studio/test/print-marks.test.ts +0 -113
  368. package/apps/studio/test/print-units.test.ts +0 -173
  369. package/apps/studio/test/read-annotations.test.ts +0 -727
  370. package/apps/studio/test/read-only-gate.test.ts +0 -188
  371. package/apps/studio/test/read-only-role.test.ts +0 -95
  372. package/apps/studio/test/readiness.test.ts +0 -337
  373. package/apps/studio/test/remote-ahead-behind-cache.test.ts +0 -89
  374. package/apps/studio/test/reorder-api.test.ts +0 -210
  375. package/apps/studio/test/repo-concurrency.test.ts +0 -146
  376. package/apps/studio/test/repo-lock.test.ts +0 -350
  377. package/apps/studio/test/report-proxy.test.ts +0 -125
  378. package/apps/studio/test/ripple.test.ts +0 -197
  379. package/apps/studio/test/runtime-bundle-error-mapping.test.ts +0 -43
  380. package/apps/studio/test/runtime-bundle.test.ts +0 -69
  381. package/apps/studio/test/sanitize-annotation-svg.test.ts +0 -238
  382. package/apps/studio/test/scaffold-design.test.ts +0 -122
  383. package/apps/studio/test/server-lifecycle.test.ts +0 -38
  384. package/apps/studio/test/session-runtime-state.test.ts +0 -226
  385. package/apps/studio/test/session-scope.test.ts +0 -100
  386. package/apps/studio/test/shared-doc-cell-pairing.test.ts +0 -642
  387. package/apps/studio/test/shared-doc-convergence.test.ts +0 -265
  388. package/apps/studio/test/shared-doc-foundation.test.ts +0 -63
  389. package/apps/studio/test/shared-doc-migrate.test.ts +0 -369
  390. package/apps/studio/test/shared-doc-projection.test.ts +0 -238
  391. package/apps/studio/test/shell-importmap.test.ts +0 -56
  392. package/apps/studio/test/sizing-mode.test.ts +0 -102
  393. package/apps/studio/test/slash-commands.test.ts +0 -117
  394. package/apps/studio/test/snap-distance-pill.test.ts +0 -68
  395. package/apps/studio/test/spacing-handles.test.ts +0 -138
  396. package/apps/studio/test/specimen-select.test.ts +0 -88
  397. package/apps/studio/test/sync-agent.test.ts +0 -660
  398. package/apps/studio/test/sync-annotations-cold-start.test.ts +0 -302
  399. package/apps/studio/test/sync-asset-push.test.ts +0 -931
  400. package/apps/studio/test/sync-atomic-write.test.ts +0 -62
  401. package/apps/studio/test/sync-attach-incremental.test.ts +0 -527
  402. package/apps/studio/test/sync-autocommit.test.ts +0 -537
  403. package/apps/studio/test/sync-canvas-path.test.ts +0 -200
  404. package/apps/studio/test/sync-cell-write-nudge.test.ts +0 -346
  405. package/apps/studio/test/sync-codec.test.ts +0 -230
  406. package/apps/studio/test/sync-cold-seed-dedup.test.ts +0 -187
  407. package/apps/studio/test/sync-cold-start.test.ts +0 -304
  408. package/apps/studio/test/sync-connect-honesty.test.ts +0 -114
  409. package/apps/studio/test/sync-connection-state.test.ts +0 -337
  410. package/apps/studio/test/sync-ctl-channel.test.ts +0 -508
  411. package/apps/studio/test/sync-decide-file.test.ts +0 -420
  412. package/apps/studio/test/sync-doc-name.test.ts +0 -281
  413. package/apps/studio/test/sync-echo-guard.test.ts +0 -96
  414. package/apps/studio/test/sync-file-ledger.test.ts +0 -334
  415. package/apps/studio/test/sync-file-membership.test.ts +0 -347
  416. package/apps/studio/test/sync-file-plane.test.ts +0 -976
  417. package/apps/studio/test/sync-file-pull.test.ts +0 -333
  418. package/apps/studio/test/sync-fresh-link-parity.test.ts +0 -267
  419. package/apps/studio/test/sync-fs-mirror.test.ts +0 -181
  420. package/apps/studio/test/sync-hardening.test.ts +0 -586
  421. package/apps/studio/test/sync-hub-listing.test.ts +0 -46
  422. package/apps/studio/test/sync-hubs-config.test.ts +0 -133
  423. package/apps/studio/test/sync-incident-replay.test.ts +0 -211
  424. package/apps/studio/test/sync-journal.test.ts +0 -176
  425. package/apps/studio/test/sync-meta-codec.test.ts +0 -199
  426. package/apps/studio/test/sync-migrate-flat-fallback.test.ts +0 -98
  427. package/apps/studio/test/sync-move-retirement.test.ts +0 -231
  428. package/apps/studio/test/sync-panel-surface.test.ts +0 -153
  429. package/apps/studio/test/sync-path-pull.test.ts +0 -531
  430. package/apps/studio/test/sync-presentation.test.ts +0 -262
  431. package/apps/studio/test/sync-pull-budget.test.ts +0 -169
  432. package/apps/studio/test/sync-remote-docs.test.ts +0 -333
  433. package/apps/studio/test/sync-resync-routes.test.ts +0 -168
  434. package/apps/studio/test/sync-runtime.test.ts +0 -1675
  435. package/apps/studio/test/sync-seed-defers-to-hub.test.ts +0 -83
  436. package/apps/studio/test/sync-settings-routes.test.ts +0 -195
  437. package/apps/studio/test/sync-settings.test.ts +0 -151
  438. package/apps/studio/test/sync-status.test.ts +0 -261
  439. package/apps/studio/test/sync-supervisor.test.ts +0 -262
  440. package/apps/studio/test/sync-tombstone-apply.test.ts +0 -111
  441. package/apps/studio/test/sync-trash.test.ts +0 -132
  442. package/apps/studio/test/sync-two-peer-discovery.test.ts +0 -343
  443. package/apps/studio/test/sync-untrusted.test.ts +0 -103
  444. package/apps/studio/test/system-endpoint.test.ts +0 -144
  445. package/apps/studio/test/text-editability-stamp.test.ts +0 -131
  446. package/apps/studio/test/timeline-comp-target.test.ts +0 -139
  447. package/apps/studio/test/timeline-media-cache.test.ts +0 -41
  448. package/apps/studio/test/timeline-parse.test.ts +0 -184
  449. package/apps/studio/test/timeline-scale.test.ts +0 -65
  450. package/apps/studio/test/timeline-snap.test.ts +0 -85
  451. package/apps/studio/test/tool-palette-insert-anchor.test.ts +0 -86
  452. package/apps/studio/test/tool-palette.test.tsx +0 -55
  453. package/apps/studio/test/tour-overlay.test.tsx +0 -90
  454. package/apps/studio/test/trusted-request-host.test.ts +0 -66
  455. package/apps/studio/test/undo-sequence-byte-compare.test.ts +0 -211
  456. package/apps/studio/test/undo-stack.test.ts +0 -244
  457. package/apps/studio/test/use-agent-presence.test.tsx +0 -117
  458. package/apps/studio/test/use-annotation-resize.test.ts +0 -402
  459. package/apps/studio/test/use-annotation-selection.test.tsx +0 -77
  460. package/apps/studio/test/use-artboard-drag.test.ts +0 -324
  461. package/apps/studio/test/use-canvas-activity.test.tsx +0 -206
  462. package/apps/studio/test/use-collab.test.ts +0 -0
  463. package/apps/studio/test/use-cursor-modifiers.test.ts +0 -59
  464. package/apps/studio/test/use-keyboard-discipline.test.ts +0 -27
  465. package/apps/studio/test/use-selection-set.test.tsx +0 -166
  466. package/apps/studio/test/use-snap-guides.test.ts +0 -271
  467. package/apps/studio/test/use-tool-mode.test.tsx +0 -186
  468. package/apps/studio/test/use-undo-stack.test.tsx +0 -223
  469. package/apps/studio/test/video-asset.test.ts +0 -163
  470. package/apps/studio/test/video-comp-fixture.test.ts +0 -50
  471. package/apps/studio/test/video-comp.test.ts +0 -270
  472. package/apps/studio/test/video-encode-lib.test.ts +0 -63
  473. package/apps/studio/test/video-render-bridge.test.ts +0 -171
  474. package/apps/studio/test/whats-new.test.ts +0 -113
  475. package/apps/studio/test/whisper-setup.test.ts +0 -97
  476. package/apps/studio/test/workspace-containment.test.ts +0 -382
  477. package/apps/studio/test/workspace-signin.test.ts +0 -292
  478. package/apps/studio/test/ws-handshake.test.ts +0 -33
  479. package/cli/commands/cache.test.mjs +0 -166
  480. package/cli/commands/design-link.test.mjs +0 -419
  481. package/cli/commands/design.test.mjs +0 -240
  482. package/cli/commands/doctor.test.mjs +0 -237
  483. package/cli/commands/hub-workspace.test.mjs +0 -275
  484. package/cli/commands/hub.test.mjs +0 -225
  485. package/cli/commands/kg.test.mjs +0 -142
  486. package/cli/commands/preflight.test.mjs +0 -46
  487. package/cli/lib/argv.test.mjs +0 -46
  488. package/cli/lib/cache.test.mjs +0 -303
  489. package/cli/lib/cell-plan.test.mjs +0 -225
  490. package/cli/lib/config-lint.test.mjs +0 -117
  491. package/cli/lib/ddr-to-kgai.test.mjs +0 -118
  492. package/cli/lib/design-ownership.test.mjs +0 -329
  493. package/cli/lib/fetch-asset.test.mjs +0 -213
  494. package/cli/lib/figma-codegen-reachability.test.mjs +0 -104
  495. package/cli/lib/figma-import-controls.test.mjs +0 -70
  496. package/cli/lib/flow-design-integration.test.mjs +0 -165
  497. package/cli/lib/gitignore-block.test.mjs +0 -136
  498. package/cli/lib/gitignore-drift.test.mjs +0 -156
  499. package/cli/lib/hubs-config.test.mjs +0 -165
  500. package/cli/lib/pkg-root.test.mjs +0 -111
  501. package/cli/lib/plugin-cli-reachability.test.mjs +0 -57
  502. package/cli/lib/plugin-name-namespace.test.mjs +0 -71
  503. package/cli/lib/reconstruct-toolset.test.mjs +0 -194
  504. package/cli/lib/stack-detect.test.mjs +0 -121
  505. package/cli/lib/update-check.test.mjs +0 -32
  506. package/cli/lib/workspace-plan.test.mjs +0 -699
@@ -139,6 +139,38 @@ const PDF_DPI_OPTIONS: readonly PdfDpiOption[] = [
139
139
  ];
140
140
  const PDF_DPI_DEFAULT = 'auto';
141
141
 
142
+ // issue #116 — how the export treats text. Mirrors app.jsx's PDF_TEXT_OPTIONS
143
+ // (same mirror obligation as PDF_DPI_OPTIONS above).
144
+ //
145
+ // The list is FIXED regardless of whether Ghostscript is present on this
146
+ // machine. Hiding "Convert to outlines" when gs is missing would need a new
147
+ // capability route, and would answer the user's question ("can I send this to
148
+ // a printer?") by making the answer invisible; a refusal that names the
149
+ // one-line install is more honest and reaches them at the moment they care.
150
+ type PdfTextOption = { id: 'keep' | 'embed' | 'outline'; label: string; description: string };
151
+ const PDF_TEXT_OPTIONS: readonly PdfTextOption[] = [
152
+ {
153
+ id: 'keep',
154
+ label: 'Keep selectable (default)',
155
+ description:
156
+ 'Text stays selectable and searchable. You are warned if a font came out unprintable.',
157
+ },
158
+ {
159
+ id: 'embed',
160
+ label: 'Verify fonts embedded',
161
+ description:
162
+ 'Same file, but the export fails instead of shipping a font a print shop would reject.',
163
+ },
164
+ {
165
+ id: 'outline',
166
+ label: 'Convert to outlines (print-safe)',
167
+ description:
168
+ 'Every glyph becomes a vector path — no fonts left to break. Text is no longer selectable ' +
169
+ 'and the file can grow a lot. Needs Ghostscript installed locally; cloud workspaces have it.',
170
+ },
171
+ ];
172
+ const PDF_TEXT_DEFAULT = 'keep';
173
+
142
174
  /**
143
175
  * The artboard under the viewport centre — the export dialog's notion of "the
144
176
  * active artboard" when nothing is selected. getBoundingClientRect is in
@@ -490,6 +522,7 @@ const DialogShell = (() => {
490
522
  const [pdfMarksCrop, setPdfMarksCrop] = useState(false);
491
523
  const [pdfMarksRegistration, setPdfMarksRegistration] = useState(false);
492
524
  const [pdfDpiId, setPdfDpiId] = useState<string>(PDF_DPI_DEFAULT);
525
+ const [pdfTextId, setPdfTextId] = useState<string>(PDF_TEXT_DEFAULT);
493
526
  // Optional — the dialog can be mounted in tests without a provider.
494
527
  const selSet = useSelectionSetOptional();
495
528
 
@@ -513,6 +546,10 @@ const DialogShell = (() => {
513
546
  };
514
547
  const pdfDpi = PDF_DPI_OPTIONS.find((d) => d.id === pdfDpiId)?.value;
515
548
  if (pdfDpi !== undefined) options.dpi = pdfDpi;
549
+ // Omitted for `keep` rather than sent explicitly: the adapter's default
550
+ // IS keep, so an untouched dialog produces the exact request body it
551
+ // did before this feature existed.
552
+ if (pdfTextId !== 'keep') options.text = pdfTextId;
516
553
  }
517
554
  onSubmit(format, scope, options);
518
555
  }, [
@@ -524,6 +561,7 @@ const DialogShell = (() => {
524
561
  pdfMarksCrop,
525
562
  pdfMarksRegistration,
526
563
  pdfDpiId,
564
+ pdfTextId,
527
565
  onSubmit,
528
566
  ]);
529
567
 
@@ -626,6 +664,22 @@ const DialogShell = (() => {
626
664
  The page stays vector; this only sets the capture density for raster content on it
627
665
  (dropped photos, large-format art).
628
666
  </p>
667
+ <label htmlFor="dc-ed-pdf-text">Text</label>
668
+ <select
669
+ id="dc-ed-pdf-text"
670
+ data-testid="export-pdf-text"
671
+ value={pdfTextId}
672
+ onChange={(e) => setPdfTextId(e.target.value)}
673
+ >
674
+ {PDF_TEXT_OPTIONS.map((t) => (
675
+ <option key={t.id} value={t.id}>
676
+ {t.label}
677
+ </option>
678
+ ))}
679
+ </select>
680
+ <p className="dc-ed-desc">
681
+ {PDF_TEXT_OPTIONS.find((t) => t.id === pdfTextId)?.description}
682
+ </p>
629
683
  <label style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}>
630
684
  <input
631
685
  type="checkbox"
@@ -7,8 +7,8 @@
7
7
  // RCA `issue-mp4-audio-export-html5audio-silent-degrade`.
8
8
 
9
9
  /**
10
- * The file is real and the job is `done` — but it is NOT what was asked for,
11
- * and saying so is the whole point.
10
+ * The file is real and the job is `done` — but it is NOT clean, and saying so
11
+ * is the whole point.
12
12
  *
13
13
  * An mp4 export of a comp using `remotion`'s `<Audio>` (= `<Html5Audio>`, which
14
14
  * `@remotion/web-renderer` rejects outright) degrades to the video-only
@@ -17,10 +17,26 @@
17
17
  * for it, and the only trace was one `console.error` in the desktop app's
18
18
  * stderr — so the artifact and its ledger entry were indistinguishable from a
19
19
  * good export. This is what makes them distinguishable.
20
+ *
21
+ * Audio was the first instance of that shape, not the only one: issue #116 is
22
+ * the same failure in a different format. A PDF whose text came out as Type 3
23
+ * fonts is downloadable, opens fine, and looks right on screen — and a print
24
+ * shop's preflight rejects it. Both are "the file is real, the file is wrong",
25
+ * so both ride THIS channel rather than a parallel one: the job record, the
26
+ * history ledger, the WS emit, the pill and the completion toast are already
27
+ * wired to it, and a second notice type would have to re-earn all five.
28
+ *
29
+ * Every cause flag is optional — a degradation carries the one(s) that apply.
20
30
  */
21
31
  export interface ExportDegradation {
22
32
  /** The requested audio track is absent from the produced file. */
23
- audioDropped: boolean;
33
+ audioDropped?: boolean;
34
+ /**
35
+ * Fonts a print shop's preflight will reject — Type 3 or genuinely
36
+ * non-embedded (issue #116). Names only, already stripped of their
37
+ * `AAAAAA+` subset prefixes; the count and the split live in `reason`.
38
+ */
39
+ fontsNotEmbedded?: string[];
24
40
  /** Human-readable cause, already newline-collapsed by the shim (DDR-054). */
25
41
  reason: string;
26
42
  /** One-line remedy — present only when the cause is a fixable authoring mistake. */
@@ -0,0 +1,194 @@
1
+ // Ghostscript — the `text: 'outline'` post-pass for PDF export (issue #116).
2
+ //
3
+ // Chromium emits Type 3 fonts for colour fonts and synthetic italics and has
4
+ // no switch to stop it; `page.pdf()` has no outline mode; and pdf-lib cannot
5
+ // convert a glyph to a path. Ghostscript's `-dNoOutputFonts` does exactly one
6
+ // thing — replace every glyph with its vector outline — and it is the only
7
+ // off-the-shelf tool that does. So this module shells out to it.
8
+ //
9
+ // WHY AN EXTERNAL BINARY, AND WHY IT IS NOT BUNDLED
10
+ // ------------------------------------------------
11
+ // Ghostscript is AGPL-3.0 and Maude is MIT. Exec'ing an unmodified `gs` the
12
+ // user (or the image) installed is clean and ordinary; shipping it INSIDE the
13
+ // signed `.app` would drag AGPL distribution obligations and third-party
14
+ // notarization into the packaged product, on every platform, for one opt-in
15
+ // export mode. So: `apps/render/Dockerfile` installs it, which makes the cloud
16
+ // lane work with no user setup; the desktop app and the npm CLI resolve it off
17
+ // PATH and fail LOUD when it is absent. Writing our own outliner was weighed
18
+ // and rejected — see the DDR; the short version is that a DOM-side pass cannot
19
+ // handle COLRv1 layering (the exact font that motivated the issue) and a
20
+ // content-stream pass means emulating the whole PDF text-state machine.
21
+ //
22
+ // FAILURE POSTURE — DELIBERATELY THE OPPOSITE OF pptx.ts
23
+ // -----------------------------------------------------
24
+ // `pptx.ts` resolves an optional `svg2pptx` the same way and, when it is
25
+ // missing, quietly falls back to a PNG deck. That is right there: the user
26
+ // asked for slides and gets slides. It would be WRONG here. A user who asked
27
+ // for outlines and silently receives a PDF with live Type 3 fonts has been
28
+ // handed the precise artifact this feature exists to prevent, and will find
29
+ // out from their printer. Missing `gs` is an error, never a fallback.
30
+
31
+ import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
32
+ import { tmpdir } from 'node:os';
33
+ import path from 'node:path';
34
+
35
+ /**
36
+ * argv prefix for Ghostscript. `MAUDE_GHOSTSCRIPT` overrides (space-separated,
37
+ * so a wrapper or an absolute path both work); otherwise the usual binary
38
+ * names — `gs` everywhere, `gswin64c` on Windows (the console build; plain
39
+ * `gswin64` opens a GUI window and never returns).
40
+ *
41
+ * Resolved against the LIVE `process.env.PATH` rather than Bun's startup
42
+ * snapshot, matching `_runtime.ts`'s resolver: the desktop sidecar corrects
43
+ * PATH at spawn time (DDR-128), and a lookup against the stale snapshot would
44
+ * miss a `gs` that is genuinely reachable.
45
+ */
46
+ export function ghostscriptArgv(): string[] | null {
47
+ const override = process.env.MAUDE_GHOSTSCRIPT?.trim();
48
+ if (override) return override.split(/\s+/);
49
+ const pathEnv = process.env.PATH;
50
+ const found = Bun.which('gs', { PATH: pathEnv }) ?? Bun.which('gswin64c', { PATH: pathEnv });
51
+ return found ? [found] : null;
52
+ }
53
+
54
+ export const GHOSTSCRIPT_MISSING_MESSAGE =
55
+ 'Outlining text needs Ghostscript, which was not found on PATH. ' +
56
+ 'Install it (macOS: `brew install ghostscript`, Debian/Ubuntu: `apt-get install ghostscript`, ' +
57
+ 'Windows: `choco install ghostscript`), or set MAUDE_GHOSTSCRIPT to its path. ' +
58
+ 'Exports run in a Maude workspace (cloud) already have it. ' +
59
+ 'Without it, use text=embed to at least verify the fonts, or text=keep for today’s behaviour.';
60
+
61
+ let _available: boolean | null = null;
62
+
63
+ /** Is a usable Ghostscript reachable? Memoized — probed at most once per process. */
64
+ export async function ghostscriptAvailable(): Promise<boolean> {
65
+ if (_available !== null) return _available;
66
+ const argv = ghostscriptArgv();
67
+ if (!argv) {
68
+ _available = false;
69
+ return false;
70
+ }
71
+ try {
72
+ const [bin, ...rest] = argv as [string, ...string[]];
73
+ const proc = Bun.spawn([bin, ...rest, '--version'], { stdout: 'pipe', stderr: 'pipe' });
74
+ _available = (await proc.exited) === 0;
75
+ } catch {
76
+ _available = false;
77
+ }
78
+ return _available;
79
+ }
80
+
81
+ /** Test seam — forget the memoized probe so a test can vary the environment. */
82
+ export function resetGhostscriptProbe(): void {
83
+ _available = null;
84
+ }
85
+
86
+ /**
87
+ * The flag set, measured rather than assumed (plan Task 9).
88
+ *
89
+ * `-dNoOutputFonts` is the feature; everything else exists to stop Ghostscript
90
+ * from helpfully ruining the raster content on the way through. Measured on a
91
+ * three-page photo PDF (1600x1200 JPEG per page, `pdfimages -list` before and
92
+ * after):
93
+ *
94
+ * | flags | size | image enc | px / ppi |
95
+ * | pass-through ON + forced Flate (this) | 144 483 | jpeg | identical |
96
+ * | pass-through ON only | 144 417 | jpeg | identical |
97
+ * | pass-through OFF + forced Flate | 417 369 | flate | identical |
98
+ *
99
+ * Two things that measurement settles:
100
+ *
101
+ * 1. **`-dPassThroughJPEGImages` is the flag that matters, and it is stated
102
+ * explicitly on purpose.** It defaults to true, which is why the issue's
103
+ * hand-run recipe did not corrupt anything — but the third row is what
104
+ * happens the moment it does not win: every photo is decoded and re-encoded
105
+ * as Flate, ~3x here and far worse on real photography (this is the
106
+ * reported 16 MB -> 254 MB). Relying on a default for that is not a plan.
107
+ * 2. **The forced Flate filters stay anyway.** `AutoFilter*` defaults to true,
108
+ * and for an image Ghostscript genuinely must re-encode (a Flate-encoded
109
+ * source, which is what Chromium often produces) "auto" is free to choose
110
+ * DCT — a LOSSY re-encode of lossless print artwork, silently. Pinning the
111
+ * filter to Flate makes the only re-encode we can be handed a lossless one.
112
+ *
113
+ * `-dDownsample*=false` is the other half of the fidelity guarantee: without
114
+ * it Ghostscript resamples to its default target and quietly destroys print
115
+ * DPI. `-dSAFER` is default-on in gs >= 9.50 but passed explicitly — the input
116
+ * is user content.
117
+ */
118
+ export const OUTLINE_FLAGS: readonly string[] = [
119
+ '-sDEVICE=pdfwrite',
120
+ '-dNoOutputFonts',
121
+ '-dCompatibilityLevel=1.7',
122
+ '-dPreserveMarkedContent=false',
123
+ '-dPassThroughJPEGImages=true',
124
+ '-dPassThroughJPXImages=true',
125
+ '-dAutoFilterColorImages=false',
126
+ '-dColorImageFilter=/FlateEncode',
127
+ '-dAutoFilterGrayImages=false',
128
+ '-dGrayImageFilter=/FlateEncode',
129
+ '-dDownsampleColorImages=false',
130
+ '-dDownsampleGrayImages=false',
131
+ '-dDownsampleMonoImages=false',
132
+ '-dBATCH',
133
+ '-dNOPAUSE',
134
+ '-dQUIET',
135
+ '-dSAFER',
136
+ ];
137
+
138
+ export interface OutlineOptions {
139
+ /** Aborts the pass and kills Ghostscript. */
140
+ signal?: AbortSignal;
141
+ /** Wall-clock ceiling. Outlining a photo-heavy print job is genuinely slow. */
142
+ timeoutSec?: number;
143
+ }
144
+
145
+ /**
146
+ * Convert every glyph in `bytes` to vector outlines.
147
+ *
148
+ * Via temp files rather than stdin/stdout: `pdfwrite` seeks in its output, so
149
+ * it cannot write to a pipe, and feeding a large PDF through stdin buys
150
+ * nothing when the output has to land on disk regardless.
151
+ *
152
+ * Throws `GHOSTSCRIPT_MISSING_MESSAGE` when no `gs` resolves, and a message
153
+ * carrying Ghostscript's own stderr when it exits non-zero. It never returns
154
+ * the input unchanged — a caller that gets bytes back gets outlined bytes.
155
+ */
156
+ export async function outlinePdf(
157
+ bytes: Uint8Array,
158
+ opts: OutlineOptions = {}
159
+ ): Promise<Uint8Array> {
160
+ const argv = ghostscriptArgv();
161
+ if (!argv || !(await ghostscriptAvailable())) throw new Error(GHOSTSCRIPT_MISSING_MESSAGE);
162
+
163
+ const dir = mkdtempSync(path.join(tmpdir(), 'maude-outline-'));
164
+ const inPath = path.join(dir, 'in.pdf');
165
+ const outPath = path.join(dir, 'out.pdf');
166
+ try {
167
+ await Bun.write(inPath, bytes);
168
+ const [bin, ...rest] = argv as [string, ...string[]];
169
+
170
+ // The caller's abort and our own timeout are one signal to the child, so a
171
+ // cancelled export and a hung Ghostscript are killed by the same path.
172
+ const timeoutSec = opts.timeoutSec ?? 600;
173
+ const signals = [AbortSignal.timeout(timeoutSec * 1000)];
174
+ if (opts.signal) signals.push(opts.signal);
175
+ const signal = AbortSignal.any(signals);
176
+
177
+ const proc = Bun.spawn([bin, ...rest, '-o', outPath, ...OUTLINE_FLAGS, inPath], {
178
+ stdout: 'pipe',
179
+ stderr: 'pipe',
180
+ signal,
181
+ });
182
+ const [code, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
183
+ if (code !== 0) {
184
+ if (opts.signal?.aborted) throw new Error('export cancelled');
185
+ const detail = stderr.trim().split('\n').slice(0, 4).join(' ').slice(0, 500);
186
+ throw new Error(
187
+ `Ghostscript failed to outline the PDF (exit ${code})${detail ? `: ${detail}` : ''}`
188
+ );
189
+ }
190
+ return new Uint8Array(readFileSync(outPath));
191
+ } finally {
192
+ rmSync(dir, { recursive: true, force: true });
193
+ }
194
+ }
@@ -0,0 +1,261 @@
1
+ // PDF font preflight — issue #116.
2
+ //
3
+ // Chromium's `page.pdf()` is vector-faithful for geometry but NOT for every
4
+ // font: a COLRv1 colour font (`@font-palette-values`) and a SYNTHETIC italic
5
+ // (CSS asks for `italic`, no real italic face is loaded) both come out as
6
+ // **Type 3** fonts. A Type 3 font is not a font program at all — each glyph is
7
+ // its own little content stream — so a print shop's preflight reports it as
8
+ // "font not embedded", Acrobat/Illustrator render it broken, and DTP has to
9
+ // re-substitute by hand. The export looked clean from every angle Maude could
10
+ // see; the only way to find out was `pdffonts` on the delivered file, or the
11
+ // printer refusing it.
12
+ //
13
+ // This module is the "see it" half. It walks the assembled document's font
14
+ // dictionaries and classifies every font, with NO external tool and NO new
15
+ // dependency — pdf-lib is already here, it just has no font-inspection API, so
16
+ // we walk the object graph by hand. The classification drives both the
17
+ // always-on notice (`text: 'keep'`) and the hard refusal (`text: 'embed'`);
18
+ // `text: 'outline'` re-runs it after Ghostscript as a self-check.
19
+ //
20
+ // Deliberately pure (a `PDFDocument` in, an array out): no I/O, no spawn, no
21
+ // clock — so it is unit-testable against hand-built fixtures the same way
22
+ // `print/marks.ts` is.
23
+
24
+ import {
25
+ PDFArray,
26
+ PDFDict,
27
+ type PDFDocument,
28
+ PDFName,
29
+ type PDFObject,
30
+ PDFRef,
31
+ PDFStream,
32
+ } from 'pdf-lib';
33
+
34
+ /**
35
+ * What a print shop's preflight will say about this font.
36
+ *
37
+ * `type3` is called out separately from `not-embedded` even though a printer
38
+ * rejects both, because the CAUSE and the remedy differ: a Type 3 font is
39
+ * Chromium telling us it had no other representation for these glyphs
40
+ * (outline it), while a genuinely non-embedded font is a font the document
41
+ * merely REFERENCES and expects the reader to have (embed or outline it).
42
+ */
43
+ export type PdfFontKind = 'embedded' | 'type3' | 'not-embedded';
44
+
45
+ export interface PdfFontInfo {
46
+ /** `/BaseFont` with the `AAAAAA+` subset prefix stripped, or a placeholder. */
47
+ name: string;
48
+ kind: PdfFontKind;
49
+ /** Raw `/Subtype` as written in the font dict (`Type0`, `TrueType`, `Type3`, ...). */
50
+ subtype: string;
51
+ /**
52
+ * This Type 3 font's base name ALSO appears in the document as a real
53
+ * embedded font — the signature of a synthetic italic (Chromium slants the
54
+ * upright face itself and emits the slanted glyphs as Type 3, while the
55
+ * unslanted runs still use the real embedded face). Worth surfacing
56
+ * separately because unlike a colour font this one is fixable at the source:
57
+ * load the real italic face and the Type 3 font disappears.
58
+ */
59
+ syntheticItalicSuspect: boolean;
60
+ }
61
+
62
+ /** `AAAAAA+Foo-Bold` -> `Foo-Bold`. A subset prefix is exactly six capitals. */
63
+ export function stripSubsetPrefix(baseFont: string): string {
64
+ return baseFont.replace(/^[A-Z]{6}\+/, '');
65
+ }
66
+
67
+ /** Read a `/Name`-valued key as a plain string (`Type0`), or null. */
68
+ function nameValue(dict: PDFDict, key: string): string | null {
69
+ const v = dict.lookup(PDFName.of(key));
70
+ return v instanceof PDFName ? v.decodeText() : null;
71
+ }
72
+
73
+ /**
74
+ * Does this FontDescriptor carry an actual font program?
75
+ *
76
+ * `/FontFile` (Type 1), `/FontFile2` (TrueType) and `/FontFile3` (CFF /
77
+ * OpenType) are the three spellings; any one present means the bytes are in
78
+ * the file. A descriptor with none — or no descriptor at all, which is how the
79
+ * standard 14 fonts appear — means the reader is expected to supply the font.
80
+ */
81
+ function descriptorHasFontProgram(descriptor: PDFDict): boolean {
82
+ for (const key of ['FontFile', 'FontFile2', 'FontFile3']) {
83
+ const v = descriptor.lookup(PDFName.of(key));
84
+ if (v instanceof PDFStream) return true;
85
+ }
86
+ return false;
87
+ }
88
+
89
+ /**
90
+ * Classify ONE font dictionary.
91
+ *
92
+ * The `Type0` branch is the one that is easy to get wrong: a composite font's
93
+ * descriptor lives on its DESCENDANT, not on the parent dict, so reading
94
+ * `/FontDescriptor` off the parent finds nothing and would misreport every
95
+ * embedded CID font in the document as not-embedded — i.e. it would fire the
96
+ * warning on exactly the fonts that are fine.
97
+ */
98
+ function classifyFontDict(font: PDFDict): PdfFontInfo | null {
99
+ const subtype = nameValue(font, 'Subtype');
100
+ if (!subtype) return null;
101
+
102
+ const baseFontRaw = nameValue(font, 'BaseFont');
103
+ // A Type 3 font is not required to have a /BaseFont at all. Chromium does set
104
+ // one, but a hand-authored or third-party PDF may not — name it after the
105
+ // subtype rather than dropping it from the report.
106
+ const name = baseFontRaw ? stripSubsetPrefix(baseFontRaw) : `(unnamed ${subtype})`;
107
+
108
+ if (subtype === 'Type3') {
109
+ return { name, kind: 'type3', subtype, syntheticItalicSuspect: false };
110
+ }
111
+
112
+ // Composite font — the real font program hangs off DescendantFonts[0].
113
+ let descriptorHost: PDFDict = font;
114
+ if (subtype === 'Type0') {
115
+ const descendants = font.lookup(PDFName.of('DescendantFonts'));
116
+ const first = descendants instanceof PDFArray ? descendants.lookup(0) : undefined;
117
+ if (first instanceof PDFDict) descriptorHost = first;
118
+ }
119
+
120
+ const descriptor = descriptorHost.lookup(PDFName.of('FontDescriptor'));
121
+ const embedded = descriptor instanceof PDFDict && descriptorHasFontProgram(descriptor);
122
+ return {
123
+ name,
124
+ kind: embedded ? 'embedded' : 'not-embedded',
125
+ subtype,
126
+ syntheticItalicSuspect: false,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Collect every font dict reachable from a resource dict, recursing through
132
+ * Form XObjects.
133
+ *
134
+ * The recursion is not theoretical here: `pdf.ts`'s `pageFit` path embeds the
135
+ * original page as a Form XObject and draws it on a fresh page, which moves
136
+ * every one of its fonts OUT of the page's own `/Font` resources and into the
137
+ * XObject's. A non-recursive walk would report "no fonts" for exactly the
138
+ * scale-to-paper exports that print shops receive.
139
+ *
140
+ * `seen` is keyed on ref numbers, so a shared resource dict (the common case —
141
+ * Chromium reuses one across pages) is walked once, and a cyclic
142
+ * XObject -> Resources -> XObject graph terminates instead of blowing the
143
+ * stack.
144
+ */
145
+ function collectFontDicts(
146
+ resources: PDFObject | undefined,
147
+ out: PDFDict[],
148
+ seen: Set<string>
149
+ ): void {
150
+ if (!(resources instanceof PDFDict)) return;
151
+
152
+ const fonts = resources.lookup(PDFName.of('Font'));
153
+ if (fonts instanceof PDFDict) {
154
+ for (const key of fonts.keys()) {
155
+ const ref = fonts.get(key);
156
+ const id = ref instanceof PDFRef ? ref.toString() : null;
157
+ if (id) {
158
+ if (seen.has(id)) continue;
159
+ seen.add(id);
160
+ }
161
+ const font = fonts.lookup(key);
162
+ if (font instanceof PDFDict) out.push(font);
163
+ }
164
+ }
165
+
166
+ const xobjects = resources.lookup(PDFName.of('XObject'));
167
+ if (xobjects instanceof PDFDict) {
168
+ for (const key of xobjects.keys()) {
169
+ const ref = xobjects.get(key);
170
+ const id = ref instanceof PDFRef ? `xo:${ref.toString()}` : null;
171
+ if (id) {
172
+ if (seen.has(id)) continue;
173
+ seen.add(id);
174
+ }
175
+ const xo = xobjects.lookup(key);
176
+ if (xo instanceof PDFStream) {
177
+ collectFontDicts(xo.dict.lookup(PDFName.of('Resources')), out, seen);
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Every distinct font in the document, classified.
185
+ *
186
+ * Deduped on `(name, kind)` rather than on dict identity: one font that
187
+ * Chromium split across pages is one line in a warning a human reads, not ten.
188
+ * The `(name, kind)` pair — rather than name alone — is deliberate, because the
189
+ * synthetic-italic case genuinely HAS the same base name twice with two
190
+ * different representations, and collapsing those would hide the very signal
191
+ * `syntheticItalicSuspect` exists to report.
192
+ */
193
+ export function analyzePdfFonts(doc: PDFDocument): PdfFontInfo[] {
194
+ const dicts: PDFDict[] = [];
195
+ const seen = new Set<string>();
196
+ for (const page of doc.getPages()) {
197
+ // `Resources()`, NOT `lookup(PDFName.of('Resources'))`: `/Resources` is an
198
+ // INHERITABLE page attribute, and a producer is free to hang one shared
199
+ // dict off the Pages node instead of repeating it on every leaf. Reading
200
+ // the leaf directly then finds nothing and reports a document with fonts
201
+ // as having none — silently turning the whole preflight off for exactly
202
+ // the multi-page print exports it exists for.
203
+ collectFontDicts(page.node.Resources(), dicts, seen);
204
+ }
205
+
206
+ const byKey = new Map<string, PdfFontInfo>();
207
+ for (const dict of dicts) {
208
+ const info = classifyFontDict(dict);
209
+ if (!info) continue;
210
+ const key = `${info.name} ${info.kind}`;
211
+ if (!byKey.has(key)) byKey.set(key, info);
212
+ }
213
+
214
+ const all = [...byKey.values()];
215
+ const embeddedNames = new Set(all.filter((f) => f.kind === 'embedded').map((f) => f.name));
216
+ for (const info of all) {
217
+ if (info.kind === 'type3' && embeddedNames.has(info.name)) info.syntheticItalicSuspect = true;
218
+ }
219
+ return all;
220
+ }
221
+
222
+ /** The fonts a print shop's preflight will reject — Type 3 and non-embedded. */
223
+ export function unprintableFonts(fonts: PdfFontInfo[]): PdfFontInfo[] {
224
+ return fonts.filter((f) => f.kind !== 'embedded');
225
+ }
226
+
227
+ /**
228
+ * The one-line problem statement + remedy for a set of bad fonts, shared by the
229
+ * `keep` notice and the `embed` refusal so the two can never drift into
230
+ * describing the same document differently.
231
+ */
232
+ export function describeFontProblem(bad: PdfFontInfo[]): { reason: string; remedy: string } {
233
+ const type3 = bad.filter((f) => f.kind === 'type3');
234
+ const missing = bad.filter((f) => f.kind === 'not-embedded');
235
+ const parts: string[] = [];
236
+ if (type3.length) {
237
+ parts.push(
238
+ `${type3.length} font${type3.length === 1 ? '' : 's'} exported as Type 3 ` +
239
+ `(${type3.map((f) => f.name).join(', ')})`
240
+ );
241
+ }
242
+ if (missing.length) {
243
+ parts.push(
244
+ `${missing.length} font${missing.length === 1 ? '' : 's'} not embedded ` +
245
+ `(${missing.map((f) => f.name).join(', ')})`
246
+ );
247
+ }
248
+ const reason = `${parts.join('; ')} — a print shop's preflight will reject ${
249
+ bad.length === 1 ? 'it' : 'them'
250
+ }.`;
251
+
252
+ const suspects = bad.filter((f) => f.syntheticItalicSuspect);
253
+ const italicNote = suspects.length
254
+ ? ` ${suspects.map((f) => f.name).join(', ')} looks like a synthetic italic — ` +
255
+ 'loading the real italic face fixes that one at the source.'
256
+ : '';
257
+ return {
258
+ reason,
259
+ remedy: `Re-export with text=outline to convert all text to curves.${italicNote}`,
260
+ };
261
+ }