Skip to content

PDF Tools: honest Edit on real-world PDFs (pdf.edit_check, plain refusals, a browser-printed test page) - #501

Merged
ymote merged 9 commits into
mainfrom
fix/pdftools-honest-edit
Oct 11, 2026
Merged

ymote merged 9 commits into
mainfrom
fix/pdftools-honest-edit

Conversation

@ymote

@ymote ymote commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Why

PDF Tools' Edit works on simple PDFs and breaks on real-world ones. On a page a web browser printed:

  • Chinese text is refused with the engine's build note ("set CRAFT_FONTS_DIR to a craft-fonts checkout").
  • The engine reads a title drawn in several fonts as one- or two-glyph "paragraphs".
  • A replacement the paragraph's subset font can't show is set in a stand-in (Helvetica or Times) that runs over its neighbours.

Nothing told the person any of this before Apply.

What

pdf service (apps/pdf/host-service):

  • pdf.edit_check (new, src/fit.rs) takes pdf.edit_text's arguments and answers {keeps_font, font, box, covers, off_page}:

    • the font the edit would be drawn in;
    • where the edited text would be;
    • how many other lines of text it would newly run into;
    • whether part of it would leave the page.

    It runs the engine's own editor (pdfcraft-edit's rewrite_block/replace_line, what text_edit runs) on a copy opened from the open document's working bytes, then drops the copy. No engine command runs and nothing is written. The engine's read-only and security checks apply. On a typical page it costs 0.4 to 0.6 ms (release); on a private 3.4 MB browser-printed page of 399 paragraphs (not committed) it costs 16 to 19 ms.

  • Plain refusals. pdf.edit_text and pdf.edit_check refuse in words for a person, still invalid: (codes::edit_refused):

    • "PDF Tools can't edit Chinese, Japanese or Korean text yet."
    • other scripts name the characters ("PDF Tools can't type “Ж” into a PDF yet.");
    • a locked or read-only PDF;
    • a stale paragraph number.
  • pdfcraft-edit joins the pins at the same ymote/pdfcraft revision. It was already in the graph, so Cargo.lock only gains one edge.

  • pdftools_fixture --edit-sample <file> writes a page as a browser prints it. The glyphs are placeholders drawn here, with no font file copied. The page has:

    • a title in five pieces, in unnamed Type 3 fonts;
    • Chinese text in an embedded CID-keyed TrueType subset.

    It reproduces all three failures. The tests hold the check to what the edit then does (same font, same box), and show that a refusal changes nothing.

PDF Tools (apps/pdftools/bundle/main.splash):

  • When it checks: the panel asks pdf.edit_check when a paragraph is clicked, and again once typing, the alignment or the colour pauses.
  • What it says: "Set in Helvetica: this paragraph's font can't show all of this text.", "Longer than its box: it runs into 2 lines of text nearby.", "Part of it falls off the page.", or the refusal in red. A line kept in pieces gets "The PDF stores this line in 5 pieces: only this one changes."
  • Apply: while there's a warning, Apply reads "Apply anyway". An edit not yet checked as it stands is checked first, and made at once only if there is nothing to say. Apply stays in view at the card's foot while the fields scroll.
  • Older engines: an engine without the check edits as before.
  • Font names read as a person would say them ("Menlo Regular", "Unnamed font").
  • Model tests cover the new pure functions. They now also parse the whole script, views and handlers included, which the model VM never ran.

Docs: apps/pdftools/README.md and README.zh-CN.md gain an "Editing text" section, a Status line and the evidence folder; apps/AGENTS.md describes the service method.

Contract change: proposal for SERVICE.md "Edit"

SERVICE.md isn't changed here, because the coordinator owns it. Proposed text:

| pdf.edit_check | {doc, page, paragraph?, line?, text, align?, color?}, as pdf.edit_text | {keeps_font, font, box, covers, off_page}: what that edit would do, without making it |

pdf.edit_check makes the edit on a copy of the open document (its unsaved edits included) with the engine's own editor, and drops the copy, so nothing changes.

  • keeps_font: whether the new text is drawn in the paragraph's (or line's) own font.
  • font: the font it would be drawn in. That is either its own name ("" when the PDF names none, as pdf.lines says) or the stand-in (Helvetica, Times or Courier, bold or italic to match).
  • box: where the edited text would be, [x, y, w, h]; null when none would show.
  • covers: how many other lines of text on the page it would run into that the paragraph doesn't touch now.
  • off_page: whether part of it would fall outside the page.

It refuses as pdf.edit_text would, in the same words.

Both methods refuse an edit the engine won't make with invalid: and words for a person:

  • text no font the engine has can show: "PDF Tools can't edit Chinese, Japanese or Korean text yet.", and other characters are named;
  • a document that opens for reading only, or whose security settings forbid changes;
  • a paragraph or line number that is no longer on the page.

What ran

  • cargo test --locked -p octosense-pdf-service: 68 passed, 1 ignored (timings); first_call 1 passed, 1 ignored; skill 1 passed.
  • cargo clippy --locked -p octosense-pdf-service --all-targets --no-deps -- -D warnings: clean.
  • cargo test --locked -p octosense-shell --lib pdftools_model: 22 passed.
  • python3 tools/setup.py --check --cargo and bash tools/check-shell-graph.sh -p octosense: ok. python3 -m unittest discover -s rom/tests -p 'test_no_local_paths.py': ok.
  • Timings in release, by hand: cargo test --release --locked -p octosense-pdf-service --lib -- --ignored --nocapture timings.
  • Shell run. A desktop shell with the real engine, on the Linux build host's private headless weston, at the runtime's 64 ms budget, light and dark, on the --edit-sample page. It ran at c50104da; the evidence is in apps/pdftools/tests/evidence/edit-20261011/.
  • Not run: a hidden shell on macOS. The maintainer's RC6 app was open when this started, and later another session was running OctoSense install/restart tests on the Mac. Edit on macOS is unverified.

Known, not changed here

  • The edit box over the page is sized when you click and doesn't grow as you type, so a longer draft shows only its last line. This predates the change.
  • The engine still splits browser-printed titles into pieces, and it can't type Chinese, Japanese or Korean characters a paragraph's font lacks. This PR makes PDF Tools say so; it doesn't fix the engine. See the coordinator's report for the font-bundling and engine-update research.

🤖 Generated with Claude Code

ymote added a commit that referenced this pull request Oct 11, 2026
The coordinator's contract text for #501: pdf.edit_check's arguments
and answer, its plain refusals, and the correction that an edited
paragraph keeps its own font only when that font can show the new text
(otherwise the engine draws a stand-in, which edit_check reports first).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymote and others added 9 commits October 11, 2026 09:59
Home's main activity is exported, so any installed app can start it with
intent extras, and Makepad copies `makepad.<NAME>` extras into the
environment. OctoSense code read several of them in release builds.

- shell: `launch_input` is the one reader of MAKEPAD_APP_CONFIG. Its test
  actions, Mail's demo mailbox and module open parameters apply only in a
  development build (cfg(dev_mode): debug or --features dev-mode); a
  release build ignores them and logs that once. Desktop --test-action
  command-line flags are unchanged.
- kernel: the launch proxy (makepad.OCTOS_PROXY) applies only in a
  development build; new `dev-mode` feature, forwarded from the shell
  through ai-host.
- AppCard: every MAKEPAD_* hook (provisioning, seeding, auto prompt, fake
  GPS, proxy, dumps) goes through `launch_var`, development builds only;
  new `dev-mode` feature, forwarded from the shell through the module.
- OpenHarmony: EntryAbility forwards no launch parameters unless
  build-home-ohos.py --dev-mode stages it (a separate bundle id required).
- Android: Home's `dev-mode` feature comes from build-home.py's
  `--dev-mode` (App Studio's, #477): standalone only, never the installed
  Home's identity under its signer, and ROM staging refuses it. So a build
  that reads launch input is never the installed Home.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Docs and scripts that passed `makepad.*` launch extras to release packages
now name a development test build: `rom/scripts/build-home.py --dev-mode`
under a separate package name on Android, `build-home-ohos.py --dev-mode`
on OpenHarmony, `--features dev-mode` for the desktop mail demo, and
`DEV_MODE=1` / `--features dev-mode` for AppCard's own tools.
`provision-appcard-llm.sh` requires PACKAGE=dev.makepad.octosense.<suffix>.
AGENTS.md states the rule; English and Chinese docs change together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A desktop's environment is the local user's, like a command-line flag, so
`MAKEPAD_APP_CONFIG` (test actions, Mail's demo mailbox, module-open
settings) and AppCard's `MAKEPAD_*` hooks are read in every desktop build.
The gate applies only on Android and OpenHarmony, where a launch Intent extra
or launch parameter can come from another app: there a release build ignores
them and only a development build reads them.

- crates/shell/src/launch_input.rs: `honoured` keys on `cfg(native_mobile)`;
  tests say a phone release build ignores it while a desktop build (any
  profile) and a phone development build read it.
- apps/appcard: `launch_set_in(mobile, development, ...)` — a desktop reads
  the hooks, a phone only in a development build.
- Docs restored to plain desktop `cargo run` commands; the phone and
  OpenHarmony guidance keeps the development-build note. AGENTS.md states the
  desktop/phone split.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Protects a release Home now, without waiting for a makepad sweep: the shell's
pinned makepad gains the Studio/OCTOS_PROXY launch-input gate from
OctoSense-org/makepad#119 as the next stacked runtime patch over the pinned
base, so a shipped Android Home ignores a Studio host or app proxy from an
Intent extra. The OpenHarmony template part of #119 is not carried here, since
Home builds its own OpenHarmony entry ability.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Text editing worked on simple PDFs and broke on real-world ones. On a page a
web browser printed, the engine reads a title drawn in several fonts as one
paragraph per piece; text a paragraph's subset font lacks is set in a
stand-in (Helvetica or Times) that runs over the pieces after it; and
Chinese text is refused with the engine's build note ("set CRAFT_FONTS_DIR
to a craft-fonts checkout").

pdf.edit_check takes pdf.edit_text's arguments and answers {keeps_font,
font, box, covers, off_page}: the font the edit would be drawn in, where the
edited text would be, how many other lines it would newly run into, and
whether it would leave the page. It runs the engine's own editor
(pdfcraft-edit's rewrite_block and replace_line, what text_edit runs) on a
copy opened from the open document's working bytes and drops it: no engine
command, nothing written, the read-only and security checks the engine
makes. pdfcraft-edit joins the pins at the same ymote/pdfcraft revision; it
was already in the graph.

pdf.edit_text and pdf.edit_check now refuse in words for a person, still
invalid: ("PDF Tools can't edit Chinese, Japanese or Korean text yet.";
other scripts name the characters; a locked or read-only PDF; a stale
paragraph number).

pdftools_fixture --edit-sample writes a page as a browser prints it: a title
in five pieces in unnamed Type 3 fonts, and Chinese text in an embedded
CID-keyed TrueType subset (glyphs drawn here as placeholders). The tests use
it to hold the check to what the edit then does (same font, same box), and
show the refusals change nothing. Release timings: a check takes 0.4-0.6 ms
on the samples and 16-19 ms on a 3.4 MB browser-printed page of 399
paragraphs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Edit panel asks the engine what the edit would do (pdf.edit_check) when
a paragraph is clicked and again when typing, the alignment or the colour
pauses, and says it under the Font box: that the paragraph's font can't show
the new text, so it is set in Helvetica, Times or Courier; that the text
needs more room and runs into N lines nearby; that part of it falls off the
page; or, in red, why the engine won't make it, in the service's plain
words. A paragraph that is one piece of a line the PDF stores in pieces says
how many. While there is a warning Apply reads "Apply anyway"; an edit not
checked as it stands is checked first and made at once only if there is
nothing to say. An older engine without the check edits as before.

The Font box names fonts as a person would ("Menlo Regular", not
"ABCDEF+Menlo-Regular"; "Unnamed font" for a browser's Type 3 fonts).

The model tests cover the new pure functions, and now also parse the whole
script, views and handlers included, which the model VM never ran: one
parse error there fails the whole app when it loads. READMEs in English and
Chinese describe Editing text and the --edit-sample page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A hidden desktop shell on a headless weston showed the notes pushing Apply
and Cancel out of the card in the shell's default window (an app area of
1292 x 662). With notes showing, the card now takes the panel's height: the
fields (Font and the notes, Alignment, Colour) scroll, and Apply and Cancel
stay at its foot. Without notes it is design 08's card, as before.

The notes are shorter, the consequence first: "Set in Helvetica: this
paragraph's font can't show all of this text.", "Longer than its box: it
runs into 2 lines of text nearby.", "The PDF stores this line in 5 pieces:
only this one changes."

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A desktop shell built at c50104d, with the real pdf engine, on the Linux
build host's private headless weston (another session was running OctoSense
on the Mac), driven over the remote bridge on the --edit-sample page, light
and dark, at the runtime's 64 ms script budget: the five-pieces note, the
font and room warnings with Apply anyway in view, the plain refusal of
Chinese text inline and on Apply, an edit made anyway (the engine's result
overlaps as the panel said) and one made at once. Frames cropped to the
app's 1292 x 662 area, a receipt, and READMEs in English and Chinese;
README's Status lists it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The coordinator's contract text for #501: pdf.edit_check's arguments
and answer, its plain refusals, and the correction that an edited
paragraph keeps its own font only when that font can show the new text
(otherwise the engine draws a stand-in, which edit_check reports first).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ymote
ymote force-pushed the fix/pdftools-honest-edit branch from 092c084 to dabda6d Compare October 11, 2026 18:16
@ymote
ymote marked this pull request as ready for review October 11, 2026 18:16
ymote added a commit that referenced this pull request Oct 11, 2026
Local CI passed on the merge train's head 37008bb (#488 → #501 → #495): two runs, the Linux host and the Mac with rom, plus GitHub's embedded-browser acceptance on this head. See the PR comment.
@ymote
ymote merged commit 28f4ccb into main Oct 11, 2026
3 of 11 checks passed
ymote added a commit that referenced this pull request Oct 11, 2026
Local CI passed on the merge train's head 37008bb (#488 → #501 → #495): two runs, the Linux host and the Mac with rom, plus GitHub's embedded-browser acceptance on this head. See the PR comment.
@ymote

ymote commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

Merged in a train (#488 → #501 → #495) on two local runs of the train head 37008bb, which contains this PR's head dabda6d. Merged in order with gh pr merge --admin --merge --match-head-commit; main's tree after the last merge equals the tested tree.

  • Run 1: tools/ci-local.sh --only all --linux-host --offload, PASSED in 22m21s (55 PASS, 8 SKIPPED). On the Linux build host: 53 steps, among them the ubuntu jobs (apps.yml services and kernel-security) and the Linux process sandbox (Landlock, seccomp). Its 8 skips are rom.yml's Linux steps, which need a JDK, the Android SDK and node; run 2 ran them.
  • Run 2: tools/ci-local.sh --only all on the Mac with JDK 17, Android SDK 35 and node 24, PASSED in 23m32s (59 PASS), nothing skipped. The train touches rom/ (Read launch-time test hooks only in development builds #488), so the two-run rule applies to it.
Run 2's table
Job Step Result Time
ci-local / drift The local mapping fits the workflows PASS -
desktop.yml / desktop Prepare the pinned framework sources PASS 1s
desktop.yml / desktop Compile the desktop with the shared runtime PASS 3m15s
desktop.yml / desktop One copy of every shell source PASS -
desktop.yml / desktop Desktop tools and scripts PASS 14s
desktop.yml / desktop In-app updater and installer handoff PASS 4s
desktop.yml / desktop One Makepad, App Hub, octos and Rinx in the locked graph PASS 5s
desktop.yml / desktop The native apps' generated places match native-apps.json PASS 0s
desktop.yml / native-host-api Prepare the pinned framework sources PASS 0s
desktop.yml / native-host-api Build the native acceptance host and pinned Hub CLI PASS 9s
desktop.yml / native-host-api Signed app tool calls a real OS permission API PASS 3s
phone.yml / home Prepare pinned sources and the reviewed runtime patch PASS 0s
phone.yml / home Validate launcher icons and Apple asset catalogs PASS 3s
phone.yml / home Compile Home and its bundled modules PASS 3m17s
phone.yml / home Verify the single runtime graph PASS 3s
phone.yml / home The native apps' generated places match native-apps.json PASS 0s
phone.yml / home The pinned octos revision PASS -
phone.yml / home Build octos (unless cached) PASS -
phone.yml / home Build Home, App Hub admission and runtime policy tests PASS 43s
phone.yml / home Test Home, App Hub admission and runtime policy PASS 40s
phone.yml / home Two-lane scenario (real kernel, scripted model) PASS 31s
phone.yml / home Build opt-in AppCard tests PASS 34s
phone.yml / home Test opt-in AppCard PASS 6s
phone.yml / home Build craft-engine relay and system-chat tests PASS 39s
phone.yml / home Test craft-engine relay and system chat PASS 50s
phone.yml / home Build linked Terminal tests PASS 32s
phone.yml / home Test linked Terminal PASS 6s
phone.yml / home Build Wasm and runtime policy tests PASS 8s
phone.yml / home Test Wasm and runtime policy PASS 16s
apps.yml / services python3 tools/setup.py PASS 1s
apps.yml / services Shared system app interface PASS -
apps.yml / services Kernel service PASS 21s
apps.yml / services OctoSense release updater PASS 6s
apps.yml / services App peers PASS 26s
apps.yml / services AI providers config PASS 10s
apps.yml / services Connected provider protocols and Linux host adapter PASS 32s
apps.yml / services L0 in-card chat (sys.chat, host side) PASS 12s
apps.yml / services System toolbox (workflow templates) PASS 1m57s
apps.yml / kernel-security python3 tools/setup.py PASS 1s
apps.yml / kernel-security The pinned octos revision PASS 0s
apps.yml / kernel-security Build octos (unless cached) PASS -
apps.yml / kernel-security Real-kernel tests PASS 30s
apps.yml / apps python3 tools/setup.py PASS 1s
apps.yml / apps Host services (Mail, AI providers, News) PASS 1m12s
apps.yml / apps Connected accounts and Rinx editor PASS 1m14s
apps.yml / apps Shell AI services (crates/ai-host) PASS 2m29s
apps.yml / apps AppCard runtime setup safeguards PASS 0s
apps.yml / apps AppCard clippy (own crates; shared runtime compiles) PASS 52s
apps.yml / apps AppCard transport and store PASS 15s
apps.yml / apps One Makepad, App Hub, octos and Rinx in the locked graph PASS 4s
rom.yml / product python3 -m unittest discover -s tests -v PASS 38s
rom.yml / product Check generated Agent Binder client PASS 0s
rom.yml / product bash -n scripts/stage-forks.sh scripts/publish-release.sh scripts/apply-to-tree.sh scripts/build-rom.sh PASS -
rom.yml / web-installer npm ci --ignore-scripts --no-audit --no-fund PASS 7s
rom.yml / web-installer npm test PASS 1s
rom.yml / web-installer npx playwright install --with-deps chromium PASS 1s
rom.yml / web-installer npm run test:browser PASS 5s
rom.yml / web-installer python3 -m unittest discover -s tests -p test_installer_manifest.py -v PASS 0s
rom.yml / web-installer bash -n scripts/release.sh PASS -

Notes:

  • GitHub runs Python 3.12; this run used Python 3.9.6
  • apps.yml:services runs on ubuntu-latest in GitHub; here on macOS, so #[cfg(target_os = "linux")] code in it is not exercised (--linux-host runs it on Linux)
  • apps.yml:kernel-security runs on ubuntu-latest in GitHub; here on macOS, so #[cfg(target_os = "linux")] code in it is not exercised (--linux-host runs it on Linux)

Run 2's first attempt on the same head stopped at one failure outside this train: crates/wasm-host/tests/runtime.rs an_endless_loop_stops_at_its_deadline_and_spends_the_instance measured 189.8 ms against its 190 ms floor (its deadline fired a tick early). The rerun passed every step.

GitHub's native Windows acceptance (embedded-browser.yml, which local CI cannot replace) passed on this head: run 38163147191. Main after the train: cf06ac5, whose tree equals the tested tree.

@ymote
ymote deleted the fix/pdftools-honest-edit branch October 11, 2026 18:29
ymote added a commit that referenced this pull request Oct 11, 2026
Moves the pdfcraft engine and its patched PDF stack from 615ff308 to
51f09eea: ymote/pdfcraft's merge of storytold/pdfcraft main (f22b5d42,
318 commits, ymote/pdfcraft#2) plus the fork's 4 MiB cap on the total of
decoded page-label prefixes (ymote/pdfcraft#3), which upstream's code
had dropped. All ten pdfcraft pins in the root Cargo.toml move, #501's
pdfcraft-edit included.

- The lock pins hayro-jbig2 to the engine's hardened copy. Its patch
  had been unused: crates.io's 0.3.1 outranked the vendored 0.3.0, so
  JBIG2 images were decoded without the engine's bounds on bitmap and
  dictionary pixels and on decoding steps that never advance. That
  also drops second copies of fearless_simd and hayro-ccitt.
  tests/skill.rs now fails if any of the five patches goes unused or
  isn't at the engine's revision.
- vectorcraft moves to 2cd3a1ca (ymote/vectorcraft#1), whose PDF import
  takes the warning the engine's hayro-interpret adds (ContentTruncated).
  The patch reaches vectorcraft-pdf too, which otherwise stops
  compiling. Only the vector skill's revision lines change.
- The pdf skill is regenerated: twelve new engine commands, each
  classified from its implementation in skill/safety-rules.json, and
  fill_sign_add is file now (its new `path` reads an image signature).
  ADR 0013's count of generated ids follows (4,569).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymote added a commit that referenced this pull request Oct 11, 2026
Moves the pdfcraft engine and its patched PDF stack from 615ff308 to
51f09eea: ymote/pdfcraft's merge of storytold/pdfcraft main (f22b5d42,
318 commits, ymote/pdfcraft#2) plus the fork's 4 MiB cap on the total of
decoded page-label prefixes (ymote/pdfcraft#3), which upstream's code
had dropped. All ten pdfcraft pins in the root Cargo.toml move, #501's
pdfcraft-edit included.

- The lock pins hayro-jbig2 to the engine's hardened copy. Its patch
  had been unused: crates.io's 0.3.1 outranked the vendored 0.3.0, so
  JBIG2 images were decoded without the engine's bounds on bitmap and
  dictionary pixels and on decoding steps that never advance. That
  also drops second copies of fearless_simd and hayro-ccitt.
  tests/skill.rs now fails if any of the five patches goes unused or
  isn't at the engine's revision.
- vectorcraft moves to 2cd3a1ca (ymote/vectorcraft#1), whose PDF import
  takes the warning the engine's hayro-interpret adds (ContentTruncated).
  The patch reaches vectorcraft-pdf too, which otherwise stops
  compiling. Only the vector skill's revision lines change.
- The pdf skill is regenerated: twelve new engine commands, each
  classified from its implementation in skill/safety-rules.json, and
  fill_sign_add is file now (its new `path` reads an image signature).
  ADR 0013's count of generated ids follows (4,569).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant