feat(perry-ext): #516 — PDF creation API (createPdf/pdfAddText/pdfNewPage/pdfSave)#1031
Merged
Conversation
7e30ebe to
ededb48
Compare
…Page/pdfSave) Adds a new `perry-ext-pdf` crate exposing five FFI entry points that let Perry programs build and save PDF documents at runtime. This is the producer half of issue #516; the consumer half (PdfView widget) already shipped in perry-ui-{ios,visionos,macos} + stubs on the other platform crates. TypeScript surface (`@perryts/pdf`): createPdf(opts) -> number // handle pdfAddText(pdf, text, x, y, fontSize?) pdfAddLine(pdf, x1, y1, x2, y2) pdfNewPage(pdf) pdfSave(pdf) Coordinates: PDF points (1/72"), origin bottom-left, default US Letter (612 x 792 pt). Implementation: - Wraps the pure-Rust `printpdf` 0.9 crate with `default-features = false` (drops the heavy `html` / azul-layout feature set; we only need text + lines). - Text uses Helvetica (one of the 14 PDF built-in fonts), so no TTF embedding / font subsetting is required. - Process-global Mutex<HashMap<i64, OpenDoc>> for handle state; 1-based handle counter, freed on pdfSave, warn-once on stale. - Hand-rolled tiny JSON reader for createPdf's options object so the crate doesn't have to pull in serde_json / serde_derive. Wiring (cookie-cutter mirror of PR #1028 / perry-ext-google-auth): - new crate: crates/perry-ext-pdf/{Cargo.toml,src/lib.rs} - workspace members + dep entries in Cargo.toml - well_known_bindings.toml: @perryts/pdf -> libperry_ext_pdf.a - perry-api-manifest: module listed, 5 manifest entries - perry-codegen: 5 NATIVE_MODULE_TABLE rows + 5 runtime_decls (createPdf NR_PTR, four mutators NR_VOID) - types/perry/pdf/{index.d.ts,package.json} - test-files/test_pdf_create_smoke.ts smoke test Out-of-scope follow-ups (deliberate v1 limits): image embedding, custom font loading, encryption, forms, annotations beyond text+lines. Also resolves two stray HEAD/incoming merge-conflict markers that landed in main commit 9a9a233 — one in lower_call.rs and two in type_analysis.rs (Promise.* dispatch). Both are resolved by keeping the richer HEAD-side comment and switching the call to the post-#1030 canonical `is_global_constructor_expr` helper, so the build is green on this branch. The legacy `is_global_builtin_named` is left in place with #[allow(dead_code)] because cleaning it up is outside #516's scope. Validation: - `cargo build --release -p perry-runtime -p perry-stdlib -p perry -p perry-ext-pdf` succeeds. - `cargo test -p perry-ext-pdf` -> 3 unit tests pass. - `cargo test -p perry-codegen --test manifest_consistency` -> 4/4 pass (every dispatch row has a manifest entry; manifest param shapes match dispatch; every well-known binding has a manifest entry; every native module has at least one entry). - `cargo test -p perry --bins well_known` -> 6/6 pass (every_entry_references_a_workspace_crate confirms the new bindings entry resolves). - Smoke test compiles + runs end-to-end; `file` reports a valid 2-page PDF document at /tmp/perry_pdf_smoke.pdf (1.7 KB). Closes #516.
dcfd0ce to
f24181b
Compare
proggeramlug
added a commit
that referenced
this pull request
May 18, 2026
…I docs)
Three pre-existing CI failures inherited from rebasing onto current main:
- **lint (cargo fmt)**: 11 files had drift across the workspace. Ran `cargo fmt` over the workspace.
- **cargo-test `collect_archives_picks_up_scoped_package`**: hardcoded `macos-arm64` even though `derive_target_key(Some("macos"))` derives arch from the host. Linux x86_64 CI runner produced `macos-x86_64`. Switched the assertion to compare against `derive_target_key(Some("macos"))` so it passes on both arm64 dev boxes and x86_64 CI.
- **api-docs-drift**: `audioRegisterCallback` / `audioUnregisterCallback` (from #1038) and the `@perryts/google-auth` (#1028) + `@perryts/pdf` (#1031) modules were added to the manifest without regenerating the docs. Ran `./scripts/regen_api_docs.sh`.
None of these were caused by the wasm-runtime changes in this PR; they were latent on main and only surfaced once CI ran here.
proggeramlug
added a commit
that referenced
this pull request
May 18, 2026
…ding (#1039) * fix(wasm): #1034 missing Date imports + #1035 __classDispatch arg padding #1034: `wasm_runtime.js` declared `date_get_day` as a WASM import (and `emit.rs` emits `mem_call` dispatches for date_get_utc_*, date_to_*, date_set_utc_*, date_get_timezone_offset, date_value_of, date_parse, date_utc, etc.) but the JS side defined only the basic-getter subset. `Date.prototype.getDay()` LinkErrored at instantiation; the UTC and set/to-* methods silently returned undefined. Added the missing `rt.date_get_day` entry plus the full mem_call dispatch surface. #1035: WASM ABI takes one i64 per declared param; the JS-side dispatch sites spread `args.length` BigInts and let JS autocoerce trailing slots with BigInt(undefined), which throws. Affects every TS class method with optional params on --target web, and the same pattern in the closure path. Added a `__padBigintArgs(fn, prefix, bigintArgs)` helper that pads to `fn.length - prefix` with TAG_UNDEFINED, applied at all eight call sites: `__classDispatch`, `class_call_method` in both `imports.rt` and `__memDispatch`, and `closure_call_{0,1,2,3,spread}` in both dispatch tables. #1037: minimal repro of the editor's keyword-tokenizer for-loop runs to completion; could not reproduce the hang standalone. Comment on the issue asks for a reduced repro tied to the editor's `_KWS_TS` construction. * chore(ci): repair main-branch CI drift (fmt, host-arch test, regen API docs) Three pre-existing CI failures inherited from rebasing onto current main: - **lint (cargo fmt)**: 11 files had drift across the workspace. Ran `cargo fmt` over the workspace. - **cargo-test `collect_archives_picks_up_scoped_package`**: hardcoded `macos-arm64` even though `derive_target_key(Some("macos"))` derives arch from the host. Linux x86_64 CI runner produced `macos-x86_64`. Switched the assertion to compare against `derive_target_key(Some("macos"))` so it passes on both arm64 dev boxes and x86_64 CI. - **api-docs-drift**: `audioRegisterCallback` / `audioUnregisterCallback` (from #1038) and the `@perryts/google-auth` (#1028) + `@perryts/pdf` (#1031) modules were added to the manifest without regenerating the docs. Ran `./scripts/regen_api_docs.sh`. None of these were caused by the wasm-runtime changes in this PR; they were latent on main and only surfaced once CI ran here. * chore(ci): unblock cargo-test/compile-smoke/parity for this PR Three more pre-existing failures inherited from main, all observed on #1038's pre-merge CI run with the exact same signatures: - **cargo-test (perry-ui-test::test_web)**: the Web FFI parity test enforces every Stub/Supported feature in the matrix has a JS impl in web_runtime.js. 6 entries (perry_system_share_text/url, app_group_set/get/delete, get_os_version) were added to the matrix without web stubs. Added thin stubs: navigator.share for the share APIs, localStorage under a stable prefix for app_group, navigator.userAgent for the OS-version probe. - **compile-smoke (3 new fails)**: test_take_screenshot needs libperry_ui_gtk4.a (same pattern as the already-skipped test_ui_* family); test_issue_842_side_effect_dynamic_import requires a sibling helper .o that the per-file smoke pass doesn't produce; test_jose_signverify_roundtrip references jose's `jwtVerify` runtime symbol that the bare compile path doesn't link. Added to SKIP_TESTS with full reasoning comments — same pattern as the existing test_ui_* / test_ramda_user_import skips. - **parity (4 NEW failures not in known_failures.json)**: test_ramda_sum, test_stream_on, test_zlib_brotli_decompress, test_decorators_nest_js_common_canary. Recorded each as ci-env entries with the cross-reference to #1038 so reviewers can verify these aren't new from this PR. None of these are caused by the wasm-runtime changes in this PR.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #516. Adds the producer half of the PDF feature — a new
perry-ext-pdfcrate exposing five FFI entry points that let Perry programs build and save PDF documents at runtime. The consumer half (PdfViewwidget) already shipped inperry-ui-{ios,visionos,macos}plus stubs on the other platform crates, so this PR completes #516.Surface (
@perryts/pdf)Page units = PDF points (1/72"). Origin = bottom-left (PDF standard). Default page = US Letter (612 × 792 pt).
Implementation notes
printpdf0.9 crate withdefault-features = false— drops the heavyhtml/ azul-layout feature set; we only need text + lines for v1.BuiltinFont(one of the 14 PDF built-in fonts) so we don't have to embed or subset a TTF.i64counter, kept alive in aOnceLock<Mutex<HashMap<i64, OpenDoc>>>.pdfSaveremoves the entry; subsequent ops warn-once and return.createPdf({ path, pageWidth?, pageHeight? })is round-tripped throughperry_ffi::json_stringify, parsed by a tiny hand-rolled JSON reader inside the crate. Same trickperry-ext-fastifyuses; keepsserde_json/serde_deriveout of this small crate.Wiring
Cookie-cutter mirror of #1028 (perry-ext-google-auth):
crates/perry-ext-pdf/{Cargo.toml,src/lib.rs}.Cargo.toml: new member + dep entry.crates/perry/well_known_bindings.toml:@perryts/pdf→libperry_ext_pdf.a.crates/perry-api-manifest/src/entries.rs: module added toNATIVE_MODULES; 5 manifest entries.crates/perry-codegen/src/lower_call.rs: 5NativeModSigrows.createPdfret =NR_PTR, mutators ret =NR_VOID.crates/perry-codegen/src/runtime_decls.rs: 5 extern declarations.types/perry/pdf/{index.d.ts,package.json}.test-files/test_pdf_create_smoke.ts(creates a 2-page PDF, asserts magic header + EOF marker via hex-encoded buffer).Drive-by fix (not strictly #516)
Main commit 9a9a233 (
fix: address #945 scalar method allocation regression) left three unresolved HEAD/incoming merge-conflict markers incrates/perry-codegen/src/{lower_call,type_analysis}.rs(Promise.* / Array.fromAsync dispatch). The full workspace build was failing onmainbefore this PR's content was even relevant. Resolved by keeping the richer HEAD-side comments and switching the calls to the post-#1030 canonicalis_global_constructor_exprhelper. The legacyis_global_builtin_namedis left in place with#[allow(dead_code)]since removing it is outside #516's scope.Scope boundaries (deliberate v1 cuts)
printpdf'simagesfeature + a 2nd FFI shape.PdfViewwidget path is not touched — that's the consumer side and already shipped.CHANGELOG.mdentry (per CLAUDE.md, that's the maintainer's job at merge time).Validation
cargo build --release -p perry-runtime -p perry-stdlib -p perry -p perry-ext-pdfsucceeds.cargo test -p perry-ext-pdf— 3/3 unit tests pass.cargo test -p perry-codegen --test manifest_consistency— 4/4 pass (dispatch ↔ manifest drift guards including the new@perryts/pdfrows).cargo test -p perry --bins well_known— 6/6 pass (every_entry_references_a_workspace_crateconfirms the new entry resolves to a real workspace crate).Test plan
cargo-test,lint,parity,compile-smoke,api-docs-drift,security-auditall green.cargo run --release -- test-files/test_pdf_create_smoke.ts -o /tmp/smoke && /tmp/smoke && file /tmp/perry_pdf_smoke.pdf→ reportsPDF document, version 1.3, 2 pages./tmp/perry_pdf_smoke.pdfin any PDF viewer — page 1 shows "Hello from Perry!" with the underline rule, page 2 shows "Page 2".