@1agh/maude 1.0.9 → 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.
- package/apps/studio/acp/bridge.ts +18 -13
- package/apps/studio/acp/probe.ts +268 -16
- package/apps/studio/acp/transcript-io.ts +394 -0
- package/apps/studio/acp/transcript.ts +103 -38
- package/apps/studio/api.ts +139 -26
- package/apps/studio/bin/_import-figma.mjs +57 -11
- package/apps/studio/bin/_seed-fixture.mjs +163 -0
- package/apps/studio/canvas-imports.ts +89 -0
- package/apps/studio/client/app.jsx +257 -7
- package/apps/studio/client/export-center.jsx +15 -7
- package/apps/studio/client/panels/ReadinessList.jsx +7 -1
- package/apps/studio/client/panels/SyncPanel.jsx +125 -4
- package/apps/studio/client/report-bug.jsx +19 -1
- package/apps/studio/client/styles/3-shell-maude.css +34 -0
- package/apps/studio/client/styles/4-components.css +26 -0
- package/apps/studio/collab/persistence.ts +132 -16
- package/apps/studio/collab/registry.ts +10 -18
- package/apps/studio/debug-bundle.ts +13 -0
- package/apps/studio/dist/client.bundle.js +1128 -1128
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/export-dialog.tsx +54 -0
- package/apps/studio/exporters/degraded.ts +19 -3
- package/apps/studio/exporters/ghostscript.ts +194 -0
- package/apps/studio/exporters/pdf-fonts.ts +261 -0
- package/apps/studio/exporters/pdf.ts +152 -3
- package/apps/studio/exporters/remote.ts +36 -1
- package/apps/studio/fs-watch.ts +45 -1
- package/apps/studio/http.ts +7 -1
- package/apps/studio/mem.ts +45 -9
- package/apps/studio/readiness.ts +42 -7
- package/apps/studio/server.ts +11 -4
- package/apps/studio/sync/agent.ts +136 -29
- package/apps/studio/sync/asset-push.ts +1 -47
- package/apps/studio/sync/codec.ts +122 -31
- package/apps/studio/sync/cold-start-apply.ts +7 -0
- package/apps/studio/sync/cold-start.ts +118 -22
- package/apps/studio/sync/comment-identity.ts +68 -0
- package/apps/studio/sync/connection-state.ts +15 -1
- package/apps/studio/sync/file-ledger.ts +117 -0
- package/apps/studio/sync/file-plane.ts +623 -27
- package/apps/studio/sync/index.ts +711 -20
- package/apps/studio/sync/journal-client.ts +13 -1
- package/apps/studio/sync/migrate-seed.ts +40 -3
- package/apps/studio/sync/presentation.ts +26 -3
- package/apps/studio/sync/retry-after.ts +169 -0
- package/apps/studio/sync/seed-progress.ts +218 -0
- package/apps/studio/sync/status.ts +122 -5
- package/apps/studio/sync/transport-error.ts +111 -0
- package/apps/studio/whats-new.json +91 -0
- package/cli/bin/maude.mjs +2 -0
- package/cli/commands/codex.mjs +191 -0
- package/cli/commands/harness.mjs +1051 -0
- package/cli/commands/help.mjs +13 -0
- package/cli/lib/design-link.mjs +190 -2
- package/cli/lib/harness/capabilities.mjs +302 -0
- package/cli/lib/harness/codex-runtime.mjs +889 -0
- package/cli/lib/harness/compatibility.mjs +27 -0
- package/cli/lib/harness/discover-claude.mjs +1393 -0
- package/cli/lib/harness/managed-state.mjs +1296 -0
- package/cli/lib/harness/model.mjs +86 -0
- package/cli/lib/harness/secrets.mjs +390 -0
- package/cli/lib/harness/targets/codex.mjs +1141 -0
- package/cli/lib/harness/targets/opencode.mjs +807 -0
- package/cli/lib/harness/transaction.mjs +1099 -0
- package/cli/lib/hubs-config.mjs +35 -8
- package/cli/templates/harness/opencode/maude-projector.ts +58 -0
- package/docs/harness-capability-matrix.md +125 -0
- package/docs/harness-environment-projection.md +95 -0
- package/package.json +32 -9
- package/plugins/flow/.claude-plugin/config.schema.json +8 -0
- package/apps/studio/.ai/cache/_stats.json +0 -7
- package/apps/studio/bin/_agent-browser-safe.test.mjs +0 -165
- package/apps/studio/bin/_curl-local.test.mjs +0 -280
- package/apps/studio/bin/_pw-launch.test.mjs +0 -90
- package/apps/studio/bin/_smart-frames.test.mjs +0 -195
- package/apps/studio/bin/_transcribe.test.mjs +0 -80
- package/apps/studio/bun.lock +0 -928
- package/apps/studio/draw/test/animate.test.ts +0 -172
- package/apps/studio/draw/test/brush.test.ts +0 -213
- package/apps/studio/draw/test/composition.test.ts +0 -141
- package/apps/studio/draw/test/geometry.test.ts +0 -199
- package/apps/studio/draw/test/gradient.test.ts +0 -167
- package/apps/studio/draw/test/layout.test.ts +0 -120
- package/apps/studio/draw/test/morph.test.ts +0 -107
- package/apps/studio/draw/test/optimize.test.ts +0 -40
- package/apps/studio/draw/test/palette.test.ts +0 -123
- package/apps/studio/draw/test/primitives.test.ts +0 -129
- package/apps/studio/draw/test/serialize-animate.test.ts +0 -142
- package/apps/studio/draw/test/serialize.test.ts +0 -105
- package/apps/studio/figma/assets.test.ts +0 -464
- package/apps/studio/figma/client.test.ts +0 -395
- package/apps/studio/figma/codegen-client.test.ts +0 -276
- package/apps/studio/figma/codegen-fonts.test.ts +0 -103
- package/apps/studio/figma/codegen-values.test.ts +0 -179
- package/apps/studio/figma/comments-to-strokes.test.ts +0 -194
- package/apps/studio/figma/fig-decode.test.ts +0 -788
- package/apps/studio/figma/fig-differential.test.ts +0 -182
- package/apps/studio/figma/fig-translator.test.ts +0 -192
- package/apps/studio/figma/fig-vector.test.ts +0 -113
- package/apps/studio/figma/from-codegen.test.ts +0 -408
- package/apps/studio/figma/sanitize.test.ts +0 -325
- package/apps/studio/figma/tailwind-map.test.ts +0 -142
- package/apps/studio/figma/to-artboard.test.ts +0 -808
- package/apps/studio/figma/to-render.test.ts +0 -180
- package/apps/studio/figma/to-strokes-roundtrip.test.ts +0 -152
- package/apps/studio/figma/to-strokes.test.ts +0 -705
- package/apps/studio/figma/to-tokens.test.ts +0 -321
- package/apps/studio/figma/url.test.ts +0 -167
- package/apps/studio/footage/schema.test.ts +0 -287
- package/apps/studio/generation/adapters/groq.test.ts +0 -80
- package/apps/studio/generation/audio-library.test.ts +0 -95
- package/apps/studio/generation/captions.test.ts +0 -78
- package/apps/studio/generation/elevenlabs.test.ts +0 -223
- package/apps/studio/generation/gemini.test.ts +0 -350
- package/apps/studio/generation/jobs.test.ts +0 -128
- package/apps/studio/generation/keys.test.ts +0 -75
- package/apps/studio/generation/prefs.test.ts +0 -95
- package/apps/studio/generation/types.test.ts +0 -123
- package/apps/studio/generation/whisper-models.test.ts +0 -179
- package/apps/studio/test/_helpers.ts +0 -108
- package/apps/studio/test/acp-activity-endpoint.test.ts +0 -183
- package/apps/studio/test/acp-activity.test.ts +0 -154
- package/apps/studio/test/acp-ai-activity.test.ts +0 -182
- package/apps/studio/test/acp-attachment-serve.test.ts +0 -74
- package/apps/studio/test/acp-bootstrap-brief.test.ts +0 -178
- package/apps/studio/test/acp-branch-guard.test.ts +0 -123
- package/apps/studio/test/acp-bridge-lifetime.test.ts +0 -475
- package/apps/studio/test/acp-bridge.test.ts +0 -301
- package/apps/studio/test/acp-capabilities.test.ts +0 -123
- package/apps/studio/test/acp-caps-bridge.test.ts +0 -279
- package/apps/studio/test/acp-commands.test.ts +0 -119
- package/apps/studio/test/acp-elicitation-bridge.test.ts +0 -480
- package/apps/studio/test/acp-elicitation.test.ts +0 -251
- package/apps/studio/test/acp-env.test.ts +0 -95
- package/apps/studio/test/acp-mode-banner.test.ts +0 -45
- package/apps/studio/test/acp-origin-gate.test.ts +0 -160
- package/apps/studio/test/acp-permission-prompt.test.ts +0 -251
- package/apps/studio/test/acp-permission.test.ts +0 -278
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +0 -89
- package/apps/studio/test/acp-session-allowed-tools.test.ts +0 -235
- package/apps/studio/test/acp-session-plugins.test.ts +0 -209
- package/apps/studio/test/acp-toolgroup.test.ts +0 -76
- package/apps/studio/test/acp-transcript-view.test.ts +0 -71
- package/apps/studio/test/acp-transcript.test.ts +0 -238
- package/apps/studio/test/acp-usage-bridge.test.ts +0 -141
- package/apps/studio/test/acp-usage.test.ts +0 -143
- package/apps/studio/test/acp-write-gate.test.ts +0 -323
- package/apps/studio/test/acp-write-scope.test.ts +0 -533
- package/apps/studio/test/active-state.test.ts +0 -186
- package/apps/studio/test/activity.test.ts +0 -189
- package/apps/studio/test/ai-activity.test.ts +0 -113
- package/apps/studio/test/annotate-write.test.ts +0 -1074
- package/apps/studio/test/annotation-strokes-command.test.ts +0 -166
- package/apps/studio/test/annotations-align.test.ts +0 -88
- package/apps/studio/test/annotations-api.test.ts +0 -145
- package/apps/studio/test/annotations-bindings.test.ts +0 -262
- package/apps/studio/test/annotations-draw-modifiers.test.ts +0 -206
- package/apps/studio/test/annotations-groups.test.ts +0 -231
- package/apps/studio/test/annotations-layer.test.ts +0 -1011
- package/apps/studio/test/annotations-roundtrip.test.ts +0 -663
- package/apps/studio/test/annotations-snap.test.ts +0 -135
- package/apps/studio/test/annotations-text-format.test.ts +0 -396
- package/apps/studio/test/artboard-activity-overlay.test.tsx +0 -56
- package/apps/studio/test/artboard-guides-overlay.test.tsx +0 -152
- package/apps/studio/test/artboard-hug-commit.test.ts +0 -36
- package/apps/studio/test/artboard-kinds.test.tsx +0 -83
- package/apps/studio/test/artboard-selection-attrs.test.ts +0 -69
- package/apps/studio/test/asset-api.test.ts +0 -149
- package/apps/studio/test/assets-s3.test.ts +0 -249
- package/apps/studio/test/binary-smoke.test.ts +0 -47
- package/apps/studio/test/boot-self-heal.test.ts +0 -87
- package/apps/studio/test/browse-posture.test.tsx +0 -107
- package/apps/studio/test/bundle-smoke.test.ts +0 -46
- package/apps/studio/test/camera-reveal.test.tsx +0 -173
- package/apps/studio/test/canvas-artifacts.test.ts +0 -125
- package/apps/studio/test/canvas-build-sandbox.test.ts +0 -69
- package/apps/studio/test/canvas-build.test.ts +0 -141
- package/apps/studio/test/canvas-create-api.test.ts +0 -676
- package/apps/studio/test/canvas-cursors.test.ts +0 -164
- package/apps/studio/test/canvas-edit.test.ts +0 -470
- package/apps/studio/test/canvas-freshness-deps.test.ts +0 -64
- package/apps/studio/test/canvas-header.test.ts +0 -127
- package/apps/studio/test/canvas-hide-chrome.test.ts +0 -88
- package/apps/studio/test/canvas-hmr-runtime.test.tsx +0 -114
- package/apps/studio/test/canvas-lib-inline.test.ts +0 -146
- package/apps/studio/test/canvas-lib-motion.test.ts +0 -131
- package/apps/studio/test/canvas-lib-resolver.test.ts +0 -111
- package/apps/studio/test/canvas-list-watch.test.ts +0 -388
- package/apps/studio/test/canvas-media-drop.test.ts +0 -187
- package/apps/studio/test/canvas-meta-api.test.ts +0 -707
- package/apps/studio/test/canvas-move-api.test.ts +0 -587
- package/apps/studio/test/canvas-origin-gate.test.ts +0 -417
- package/apps/studio/test/canvas-pipeline.test.ts +0 -180
- package/apps/studio/test/canvas-rects.test.ts +0 -198
- package/apps/studio/test/canvas-reorder.test.ts +0 -211
- package/apps/studio/test/canvas-route.test.ts +0 -179
- package/apps/studio/test/canvas-shell-base.test.ts +0 -107
- package/apps/studio/test/canvas-shell-build-error.test.ts +0 -52
- package/apps/studio/test/canvas-url.test.ts +0 -138
- package/apps/studio/test/canvas-wheel-delta.test.ts +0 -42
- package/apps/studio/test/canvas-zoom-floor.test.ts +0 -73
- package/apps/studio/test/capture-core.test.ts +0 -32
- package/apps/studio/test/capture-determinism-shape.test.ts +0 -135
- package/apps/studio/test/chat-attachments.test.ts +0 -117
- package/apps/studio/test/chat-context.test.ts +0 -129
- package/apps/studio/test/chat-markdown.test.tsx +0 -58
- package/apps/studio/test/clip-addressing.test.ts +0 -738
- package/apps/studio/test/clip-ops.test.ts +0 -922
- package/apps/studio/test/cloud-build.test.ts +0 -95
- package/apps/studio/test/cloud-connect-note.test.ts +0 -135
- package/apps/studio/test/cloud-endpoints.test.ts +0 -590
- package/apps/studio/test/cloud-history-hardening.test.ts +0 -165
- package/apps/studio/test/cloud-history-posture.test.ts +0 -230
- package/apps/studio/test/cloud-managed-save-surfaces.test.ts +0 -90
- package/apps/studio/test/cloud-renew.test.ts +0 -205
- package/apps/studio/test/cloud-session-role.test.ts +0 -161
- package/apps/studio/test/cloud-shell-surfaces.test.ts +0 -170
- package/apps/studio/test/cold-start-apply.test.ts +0 -303
- package/apps/studio/test/collab-annotations-bridge.test.ts +0 -55
- package/apps/studio/test/collab-awareness-bridge.test.ts +0 -223
- package/apps/studio/test/collab-bridge.test.ts +0 -79
- package/apps/studio/test/collab-loopback.test.ts +0 -63
- package/apps/studio/test/collab-origin-gate.test.ts +0 -323
- package/apps/studio/test/collab-protocol.test.ts +0 -146
- package/apps/studio/test/collab-readonly-gate.test.ts +0 -194
- package/apps/studio/test/collab-reseed-guard.test.ts +0 -78
- package/apps/studio/test/collab-room.test.ts +0 -188
- package/apps/studio/test/collab-session-survival.test.tsx +0 -176
- package/apps/studio/test/collab-stress.test.ts +0 -129
- package/apps/studio/test/comment-mount.test.ts +0 -97
- package/apps/studio/test/comment-relay-origin-gate.test.ts +0 -117
- package/apps/studio/test/comments-api.test.ts +0 -277
- package/apps/studio/test/comments-fs-rebroadcast.test.ts +0 -155
- package/apps/studio/test/comments-overlay.test.ts +0 -117
- package/apps/studio/test/compile-entry.test.ts +0 -133
- package/apps/studio/test/component-map.test.ts +0 -48
- package/apps/studio/test/config-projection.test.ts +0 -104
- package/apps/studio/test/config-reload.test.ts +0 -230
- package/apps/studio/test/config-version.test.ts +0 -88
- package/apps/studio/test/context-resolve-tokens.test.ts +0 -97
- package/apps/studio/test/convert-to-absolute.test.ts +0 -333
- package/apps/studio/test/credential-grammar.test.ts +0 -107
- package/apps/studio/test/csp-canvas-shell.test.ts +0 -62
- package/apps/studio/test/csrf-write-guard.test.ts +0 -141
- package/apps/studio/test/debug-bundle.test.ts +0 -105
- package/apps/studio/test/design-setup-readiness.test.ts +0 -155
- package/apps/studio/test/detach-component.test.ts +0 -94
- package/apps/studio/test/dns-rebinding-guard.test.ts +0 -101
- package/apps/studio/test/dom-selection.test.ts +0 -198
- package/apps/studio/test/dynamic-text-edit.test.ts +0 -162
- package/apps/studio/test/edit-css-occurrence.test.ts +0 -81
- package/apps/studio/test/edit-persistence.test.ts +0 -91
- package/apps/studio/test/edit-scope-api.test.ts +0 -119
- package/apps/studio/test/edit-source-command.test.ts +0 -142
- package/apps/studio/test/edit-suppress.test.ts +0 -170
- package/apps/studio/test/editing-presence.test.ts +0 -103
- package/apps/studio/test/element-resize.test.ts +0 -136
- package/apps/studio/test/element-structural-api.test.ts +0 -434
- package/apps/studio/test/element-structural-edit.test.ts +0 -750
- package/apps/studio/test/ensure-browser.test.ts +0 -92
- package/apps/studio/test/equal-spacing-detector.test.ts +0 -257
- package/apps/studio/test/export-assemble.test.ts +0 -53
- package/apps/studio/test/export-browser-lane.test.ts +0 -126
- package/apps/studio/test/export-capture-fidelity.test.ts +0 -146
- package/apps/studio/test/export-capture-hygiene.test.ts +0 -675
- package/apps/studio/test/export-center.test.tsx +0 -287
- package/apps/studio/test/export-e2e-lanes.test.ts +0 -857
- package/apps/studio/test/export-format-scope-coherence.test.ts +0 -89
- package/apps/studio/test/export-lane.test.ts +0 -245
- package/apps/studio/test/export-shim-multi-capture.test.ts +0 -83
- package/apps/studio/test/exporters/canva.test.ts +0 -63
- package/apps/studio/test/exporters/degraded-propagation.test.ts +0 -123
- package/apps/studio/test/exporters/endpoint.test.ts +0 -121
- package/apps/studio/test/exporters/history.test.ts +0 -107
- package/apps/studio/test/exporters/html.test.ts +0 -26
- package/apps/studio/test/exporters/jobs.test.ts +0 -342
- package/apps/studio/test/exporters/pdf.test.ts +0 -85
- package/apps/studio/test/exporters/png.test.ts +0 -103
- package/apps/studio/test/exporters/pptx-deck.test.ts +0 -112
- package/apps/studio/test/exporters/pptx.test.ts +0 -31
- package/apps/studio/test/exporters/pw-launch.test.ts +0 -49
- package/apps/studio/test/exporters/runtime.test.ts +0 -59
- package/apps/studio/test/exporters/scope.test.ts +0 -292
- package/apps/studio/test/exporters/svg.test.ts +0 -29
- package/apps/studio/test/exporters/unsupported-media.test.ts +0 -123
- package/apps/studio/test/exporters/zip.test.ts +0 -104
- package/apps/studio/test/fetch-asset-gate.test.ts +0 -189
- package/apps/studio/test/figjam-v3-model.test.ts +0 -342
- package/apps/studio/test/figma-explode.test.ts +0 -438
- package/apps/studio/test/figma-provenance.test.ts +0 -108
- package/apps/studio/test/figma-routes.test.ts +0 -294
- package/apps/studio/test/file-lock.test.ts +0 -84
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +0 -13
- package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +0 -1
- package/apps/studio/test/fixtures/mock-acp-agent-caps.mjs +0 -158
- package/apps/studio/test/fixtures/mock-acp-agent-commands.mjs +0 -44
- package/apps/studio/test/fixtures/mock-acp-agent-elicit-flood.mjs +0 -46
- package/apps/studio/test/fixtures/mock-acp-agent-elicit-url.mjs +0 -42
- package/apps/studio/test/fixtures/mock-acp-agent-elicit.mjs +0 -63
- package/apps/studio/test/fixtures/mock-acp-agent-permission-flood.mjs +0 -51
- package/apps/studio/test/fixtures/mock-acp-agent-permission.mjs +0 -50
- package/apps/studio/test/fixtures/mock-acp-agent-slow.mjs +0 -45
- package/apps/studio/test/fixtures/mock-acp-agent-usage.mjs +0 -56
- package/apps/studio/test/fixtures/mock-acp-agent-wedged.mjs +0 -37
- package/apps/studio/test/fixtures/mock-acp-agent-write.mjs +0 -118
- package/apps/studio/test/fixtures/mock-acp-agent.mjs +0 -69
- package/apps/studio/test/fixtures/perf-canvas.mjs +0 -201
- package/apps/studio/test/fixtures/phase-20-annotations.svg +0 -1
- package/apps/studio/test/fixtures/phase-21-annotations.svg +0 -1
- package/apps/studio/test/fixtures/tiny-subset.woff2 +0 -0
- package/apps/studio/test/fixtures/video-comp-fixture.tsx +0 -82
- package/apps/studio/test/footage-store.test.ts +0 -100
- package/apps/studio/test/fs-mkdir-api.test.ts +0 -248
- package/apps/studio/test/fs-watch.test.ts +0 -41
- package/apps/studio/test/gemma-models.test.ts +0 -245
- package/apps/studio/test/generate-route.test.ts +0 -106
- package/apps/studio/test/git-api.test.ts +0 -0
- package/apps/studio/test/git-branches.test.ts +0 -341
- package/apps/studio/test/git-cloud-posture.test.ts +0 -59
- package/apps/studio/test/git-grouping.test.ts +0 -106
- package/apps/studio/test/git-lifecycle.test.ts +0 -100
- package/apps/studio/test/git-log-format.test.ts +0 -95
- package/apps/studio/test/git-safe-rel.test.ts +0 -132
- package/apps/studio/test/git-system-engine.test.ts +0 -135
- package/apps/studio/test/git-watch.test.ts +0 -97
- package/apps/studio/test/github-api.test.ts +0 -577
- package/apps/studio/test/grid-track-handles.test.ts +0 -160
- package/apps/studio/test/handoff-static-frames.test.ts +0 -215
- package/apps/studio/test/handoff.test.ts +0 -329
- package/apps/studio/test/history-rollback.test.ts +0 -88
- package/apps/studio/test/hmr-broadcast.test.ts +0 -236
- package/apps/studio/test/hmr-classify.test.ts +0 -87
- package/apps/studio/test/hub-link.test.ts +0 -80
- package/apps/studio/test/identity-cache.test.ts +0 -83
- package/apps/studio/test/import-asset-browser.test.ts +0 -64
- package/apps/studio/test/import-asset-route.test.ts +0 -71
- package/apps/studio/test/import-asset.test.ts +0 -427
- package/apps/studio/test/import-brand-browser.test.ts +0 -88
- package/apps/studio/test/import-brand-route.test.ts +0 -120
- package/apps/studio/test/import-brand.test.ts +0 -206
- package/apps/studio/test/import-figma.test.ts +0 -667
- package/apps/studio/test/import-tokens.test.ts +0 -722
- package/apps/studio/test/index-data-canvas-ds.test.ts +0 -97
- package/apps/studio/test/input-router.test.ts +0 -757
- package/apps/studio/test/inspect-script-syntax.test.ts +0 -53
- package/apps/studio/test/inspect-selections.test.ts +0 -219
- package/apps/studio/test/knob-props-authored.test.ts +0 -87
- package/apps/studio/test/layers-synthetic-groups.test.ts +0 -96
- package/apps/studio/test/locator.test.ts +0 -214
- package/apps/studio/test/marquee-overlay.test.ts +0 -94
- package/apps/studio/test/media-commit-chain.test.ts +0 -210
- package/apps/studio/test/mode-toggle.test.tsx +0 -226
- package/apps/studio/test/motion-sample.test.ts +0 -35
- package/apps/studio/test/move-artboards-command.test.ts +0 -108
- package/apps/studio/test/participants-chrome.test.ts +0 -65
- package/apps/studio/test/paths.test.ts +0 -88
- package/apps/studio/test/pdf-print-boxes.test.ts +0 -359
- package/apps/studio/test/peer-selection-follows-camera.test.tsx +0 -131
- package/apps/studio/test/perf-harness.ts +0 -193
- package/apps/studio/test/phase-3.6-smoke.test.ts +0 -79
- package/apps/studio/test/photo-bg-remove-validation.test.ts +0 -71
- package/apps/studio/test/photo-canvas-bundle.test.ts +0 -79
- package/apps/studio/test/photo-edit-api.test.ts +0 -201
- package/apps/studio/test/photo-filters.test.ts +0 -149
- package/apps/studio/test/photo-pipeline.test.ts +0 -106
- package/apps/studio/test/photo-taxonomy.test.ts +0 -96
- package/apps/studio/test/print-marks.test.ts +0 -113
- package/apps/studio/test/print-units.test.ts +0 -173
- package/apps/studio/test/read-annotations.test.ts +0 -727
- package/apps/studio/test/read-only-gate.test.ts +0 -188
- package/apps/studio/test/read-only-role.test.ts +0 -95
- package/apps/studio/test/readiness.test.ts +0 -226
- package/apps/studio/test/remote-ahead-behind-cache.test.ts +0 -89
- package/apps/studio/test/reorder-api.test.ts +0 -210
- package/apps/studio/test/repo-concurrency.test.ts +0 -146
- package/apps/studio/test/repo-lock.test.ts +0 -350
- package/apps/studio/test/report-proxy.test.ts +0 -125
- package/apps/studio/test/ripple.test.ts +0 -197
- package/apps/studio/test/runtime-bundle-error-mapping.test.ts +0 -43
- package/apps/studio/test/runtime-bundle.test.ts +0 -69
- package/apps/studio/test/sanitize-annotation-svg.test.ts +0 -238
- package/apps/studio/test/scaffold-design.test.ts +0 -122
- package/apps/studio/test/server-lifecycle.test.ts +0 -38
- package/apps/studio/test/session-runtime-state.test.ts +0 -226
- package/apps/studio/test/session-scope.test.ts +0 -100
- package/apps/studio/test/shared-doc-cell-pairing.test.ts +0 -642
- package/apps/studio/test/shared-doc-convergence.test.ts +0 -265
- package/apps/studio/test/shared-doc-foundation.test.ts +0 -63
- package/apps/studio/test/shared-doc-migrate.test.ts +0 -369
- package/apps/studio/test/shared-doc-projection.test.ts +0 -238
- package/apps/studio/test/shell-importmap.test.ts +0 -56
- package/apps/studio/test/sizing-mode.test.ts +0 -102
- package/apps/studio/test/slash-commands.test.ts +0 -117
- package/apps/studio/test/snap-distance-pill.test.ts +0 -68
- package/apps/studio/test/spacing-handles.test.ts +0 -138
- package/apps/studio/test/specimen-select.test.ts +0 -88
- package/apps/studio/test/sync-agent.test.ts +0 -660
- package/apps/studio/test/sync-annotations-cold-start.test.ts +0 -302
- package/apps/studio/test/sync-asset-push.test.ts +0 -931
- package/apps/studio/test/sync-atomic-write.test.ts +0 -62
- package/apps/studio/test/sync-attach-incremental.test.ts +0 -527
- package/apps/studio/test/sync-autocommit.test.ts +0 -537
- package/apps/studio/test/sync-canvas-path.test.ts +0 -200
- package/apps/studio/test/sync-cell-write-nudge.test.ts +0 -346
- package/apps/studio/test/sync-codec.test.ts +0 -230
- package/apps/studio/test/sync-cold-seed-dedup.test.ts +0 -187
- package/apps/studio/test/sync-cold-start.test.ts +0 -304
- package/apps/studio/test/sync-connect-honesty.test.ts +0 -114
- package/apps/studio/test/sync-connection-state.test.ts +0 -337
- package/apps/studio/test/sync-ctl-channel.test.ts +0 -508
- package/apps/studio/test/sync-decide-file.test.ts +0 -420
- package/apps/studio/test/sync-doc-name.test.ts +0 -281
- package/apps/studio/test/sync-echo-guard.test.ts +0 -96
- package/apps/studio/test/sync-file-ledger.test.ts +0 -334
- package/apps/studio/test/sync-file-membership.test.ts +0 -347
- package/apps/studio/test/sync-file-plane.test.ts +0 -976
- package/apps/studio/test/sync-file-pull.test.ts +0 -333
- package/apps/studio/test/sync-fresh-link-parity.test.ts +0 -267
- package/apps/studio/test/sync-fs-mirror.test.ts +0 -181
- package/apps/studio/test/sync-hardening.test.ts +0 -586
- package/apps/studio/test/sync-hub-listing.test.ts +0 -46
- package/apps/studio/test/sync-hubs-config.test.ts +0 -133
- package/apps/studio/test/sync-incident-replay.test.ts +0 -211
- package/apps/studio/test/sync-journal.test.ts +0 -176
- package/apps/studio/test/sync-meta-codec.test.ts +0 -199
- package/apps/studio/test/sync-migrate-flat-fallback.test.ts +0 -98
- package/apps/studio/test/sync-move-retirement.test.ts +0 -231
- package/apps/studio/test/sync-panel-surface.test.ts +0 -153
- package/apps/studio/test/sync-path-pull.test.ts +0 -531
- package/apps/studio/test/sync-presentation.test.ts +0 -262
- package/apps/studio/test/sync-pull-budget.test.ts +0 -169
- package/apps/studio/test/sync-remote-docs.test.ts +0 -333
- package/apps/studio/test/sync-resync-routes.test.ts +0 -168
- package/apps/studio/test/sync-runtime.test.ts +0 -1675
- package/apps/studio/test/sync-seed-defers-to-hub.test.ts +0 -83
- package/apps/studio/test/sync-settings-routes.test.ts +0 -195
- package/apps/studio/test/sync-settings.test.ts +0 -151
- package/apps/studio/test/sync-status.test.ts +0 -261
- package/apps/studio/test/sync-supervisor.test.ts +0 -262
- package/apps/studio/test/sync-tombstone-apply.test.ts +0 -111
- package/apps/studio/test/sync-trash.test.ts +0 -132
- package/apps/studio/test/sync-two-peer-discovery.test.ts +0 -343
- package/apps/studio/test/sync-untrusted.test.ts +0 -103
- package/apps/studio/test/system-endpoint.test.ts +0 -144
- package/apps/studio/test/text-editability-stamp.test.ts +0 -131
- package/apps/studio/test/timeline-comp-target.test.ts +0 -139
- package/apps/studio/test/timeline-media-cache.test.ts +0 -41
- package/apps/studio/test/timeline-parse.test.ts +0 -184
- package/apps/studio/test/timeline-scale.test.ts +0 -65
- package/apps/studio/test/timeline-snap.test.ts +0 -85
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +0 -86
- package/apps/studio/test/tool-palette.test.tsx +0 -55
- package/apps/studio/test/tour-overlay.test.tsx +0 -90
- package/apps/studio/test/trusted-request-host.test.ts +0 -66
- package/apps/studio/test/undo-sequence-byte-compare.test.ts +0 -211
- package/apps/studio/test/undo-stack.test.ts +0 -244
- package/apps/studio/test/use-agent-presence.test.tsx +0 -117
- package/apps/studio/test/use-annotation-resize.test.ts +0 -402
- package/apps/studio/test/use-annotation-selection.test.tsx +0 -77
- package/apps/studio/test/use-artboard-drag.test.ts +0 -324
- package/apps/studio/test/use-canvas-activity.test.tsx +0 -206
- package/apps/studio/test/use-collab.test.ts +0 -0
- package/apps/studio/test/use-cursor-modifiers.test.ts +0 -59
- package/apps/studio/test/use-keyboard-discipline.test.ts +0 -27
- package/apps/studio/test/use-selection-set.test.tsx +0 -166
- package/apps/studio/test/use-snap-guides.test.ts +0 -271
- package/apps/studio/test/use-tool-mode.test.tsx +0 -186
- package/apps/studio/test/use-undo-stack.test.tsx +0 -223
- package/apps/studio/test/video-asset.test.ts +0 -163
- package/apps/studio/test/video-comp-fixture.test.ts +0 -50
- package/apps/studio/test/video-comp.test.ts +0 -270
- package/apps/studio/test/video-encode-lib.test.ts +0 -63
- package/apps/studio/test/video-render-bridge.test.ts +0 -171
- package/apps/studio/test/whats-new.test.ts +0 -113
- package/apps/studio/test/whisper-setup.test.ts +0 -97
- package/apps/studio/test/workspace-containment.test.ts +0 -382
- package/apps/studio/test/workspace-signin.test.ts +0 -292
- package/apps/studio/test/ws-handshake.test.ts +0 -33
- package/cli/commands/cache.test.mjs +0 -166
- package/cli/commands/design-link.test.mjs +0 -419
- package/cli/commands/design.test.mjs +0 -240
- package/cli/commands/doctor.test.mjs +0 -237
- package/cli/commands/hub-workspace.test.mjs +0 -275
- package/cli/commands/hub.test.mjs +0 -225
- package/cli/commands/kg.test.mjs +0 -142
- package/cli/commands/preflight.test.mjs +0 -46
- package/cli/lib/argv.test.mjs +0 -46
- package/cli/lib/cache.test.mjs +0 -303
- package/cli/lib/cell-plan.test.mjs +0 -225
- package/cli/lib/config-lint.test.mjs +0 -117
- package/cli/lib/ddr-to-kgai.test.mjs +0 -118
- package/cli/lib/design-ownership.test.mjs +0 -329
- package/cli/lib/fetch-asset.test.mjs +0 -213
- package/cli/lib/figma-codegen-reachability.test.mjs +0 -104
- package/cli/lib/figma-import-controls.test.mjs +0 -70
- package/cli/lib/flow-design-integration.test.mjs +0 -165
- package/cli/lib/gitignore-block.test.mjs +0 -136
- package/cli/lib/gitignore-drift.test.mjs +0 -156
- package/cli/lib/hubs-config.test.mjs +0 -165
- package/cli/lib/pkg-root.test.mjs +0 -111
- package/cli/lib/plugin-cli-reachability.test.mjs +0 -57
- package/cli/lib/plugin-name-namespace.test.mjs +0 -71
- package/cli/lib/reconstruct-toolset.test.mjs +0 -194
- package/cli/lib/stack-detect.test.mjs +0 -121
- package/cli/lib/update-check.test.mjs +0 -32
- 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
|
|
11
|
-
*
|
|
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
|
|
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
|
+
}
|