Repository navigation
Conversation
Encode tabs and line feeds as character references so XML normalization cannot change field names, comment identities or metadata. Add failing-before-fix saved-PDF round-trip regressions. Co-Authored-By: Codex <noreply@openai.com>
Resolve independent progress-document conflicts while retaining upstream updates and the existing regression-tested fix. Refresh the relevant milestone notes; feature scores remain unchanged. Fresh checks: cargo fmt --all -- --check, git diff --check, and layering/assets/parity gates. Focused production/test files and lockfiles match the prior green CI head. Full CI will rerun on this merge.
Resolve independent progress-document conflicts while retaining upstream updates and the existing regression-tested fix. Refresh the relevant milestone notes; feature scores remain unchanged. Fresh checks: cargo fmt --all -- --check, git diff --check, and layering/assets/parity gates. Focused production/test files and lockfiles match the prior green CI head. Full CI will rerun on this merge.
…n folder An attachment opened as a tab is named with the same plain file name as its Save suggestion, and Export certificate names the file the same way from the certificate subject. Writing files into a chosen folder refuses any name with a separator, a drive, or that is . or .., so no caller can write outside the folder.
GoToR file specifications go through craft-docpath: network, device and object-manager paths (\??\, /??/) are refused without touching the filesystem, relative targets resolve against the source document's folder, and a result that is not absolute is refused rather than read against the working directory. Files named on the command line open under their absolute path, so their relative links keep resolving.
craft-libs#169 is merged, so the fork pin is no longer needed. Pinned to storytold/craft-libs 82a24b006eefa036b43c09a67fb9a3fbd17ffe63 (main after the merge) rather than the older 12281d07, which predates resolve_absolute and the Windows colon refusal.
Resolve remote links and document-supplied output names safely
RUSTSEC-2026-0370/0371/0372 were published today for zvariant, zbus_names and zvariant_derive. All three are unmaintained notices rather than vulnerabilities, and none has a fixed release to move to. They reach the tree as the D-Bus stack behind accesskit_unix -> atspi (Linux accessibility) and as a dev-dependency of pdfcraft-platform, so nothing ships in the other targets' binaries. This is why main went red today; no dependency version changed.
…nderer does (storytold#3081) Pieces::join separated a page's content streams with a newline, which ended a comment left open at the end of one stream. hayro, MuPDF and Poppler join them with a space, so the comment runs on into the next stream and hides its first line. Edit text, object_list, the redaction audit, redaction and the Office/HTML export therefore saw text and boxes that nothing draws, and redacting unrelated text on the page made the hidden content visible: the rewritten stream dropped the comment. - Pieces::join uses a space, so every reader of the joined streams agrees with rendering. - The parser records, per piece, whether a top-level comment runs into it and out of it (boundaries only, not every comment). A redaction rewrite of a piece ends an incoming comment before its first operator and leaves an outgoing one open, so hidden content stays hidden. - Reading for export parses the streams joined, instead of one by one. - The redaction tag check compares the joined streams the same way. - Pieces::piece_of uses a binary search.
…ier wrapper (storytold#3084) place_tagged wrapped the page's content in q/Q only when no Wrap stream existed yet. Content appended after an earlier wrapper (by another tool, or an older OCR layer) that left a cm, a clip or text state set then moved, hid or distorted every later header, Bates number, watermark, added text, flattened annotation and redaction cover. Placing a mark on top now reads the page's actual operators instead of the /PCMark tags. When the content parses cleanly with balanced q/Q and closed text objects and paths, the stretch from the first stream that leaves state set is wrapped, once, at a stream boundary no comment runs into. Content that is already self-contained gets no new wrapper, so repeated marks don't pile up. Content that can't be read that precisely (stray bytes, unbalanced q/Q, an unfinished text object or path, too large) is wrapped whole as before, rather than refusing: redaction stamps its covers after removing the text and can't back out. A q that content leaves open is closed after it, where closing it changes nothing already drawn. The wrapper's close now starts with a newline, so a comment at the end of the content before it can't swallow the Q. Removing marks strips only wrapper pairs that end the list, keeping one that later content still relies on, and recognises the older close bytes too.
M6: preserve complete plain XML form values
deny: acknowledge the new zbus unmaintained advisories
# Conflicts: # ROADMAP.md # docs/roadmap.md
# Conflicts: # ROADMAP.md # crates/xfdf/README.md # docs/roadmap.md # parity/acrobat-features.toml
…tribute-whitespace M6: preserve whitespace in XML and XFDF attributes
# Conflicts: # ROADMAP.md # docs/roadmap.md
…storytold#3081) reading_lines grouped the joined operators by the stream their first token is in and walked each group on its own. A marked-content sequence opened in one stream (its /ActualText dictionary, or the whole BDC) and closed in the next lost its replacement text: the export read "Glyphs" where every reader of the joined streams reads "Replacement text". The page's operators are now walked once. Each shown text still counts as the stream its operator starts in, and text from another stream still starts a new line, as before.
…utput-limit fix(filters): preserve output-limit errors in raw Flate fallback
…aries-and-mark-isolation Read stream-boundary comments as the renderer does, and isolate new marks from appended graphics state
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…told#2812) Seven tests failed at random on slow or busy machines, main included (FreeBSD run 38834256439 at 5fdd820). They now count the work or wait for the state they check; no assertion is loosened and nothing retries. - spot_colour_images_run_their_tint_transform_once_per_sample_value counts tint-transform runs (exactly 256, not one per pixel) through vendored hayro-interpret patch (34), color::tint_transform_runs, a thread-local counter, instead of a 10 s deadline. - shadings_with_large_sampled_functions_paint_quickly: no counter tells the flat table from the hash map (both read every sample per build), so the build each paint does is timed against reading its samples on the same thread, fastest of 8 rounds each, and must stay under 10x (flat table 1.1x, hash map 109x), instead of a 10 s deadline. - page_contents_and_nested_forms_share_one_budget: the budget is checked by its warnings; the deadline is only a hang guard, now 600 s. - The pen-stroke tests hold finished renders back from the view (DocView::hold_rasters, for tests) while they look at the settling stroke, then wait by state for its raster. - files_and_quit_from_the_operating_system waits for the tab before deleting the file the open worker reads. - image_pixels_follow_the_frame_while_a_corner_is_dragged waits for the page raster on screen before each drag. Product fix (storytold#1858): a released drawing stayed painted only until any raster of its page arrived. In full screen its edit is applied a frame later, so a raster already under way could end the preview first. A settling drawing is now marked committed when its AddAnnotation lands, only rasters after that end its preview, and a failed edit drops it.
…torytold#3046) (storytold#3050) The print-ready PDF copied the document's /OCProperties as they are, so it printed the layers in their default state: a layer hidden in the Layers panel disappeared on screen and came back on paper. Print as image already used the view's layer choices (it renders with the document's config). Once the layers have been changed, the copy imposed for printing now has its default configuration (/D) set to them: /BaseState /ON, /ON the visible layers, /OFF the hidden ones, its other entries kept. With the layers untouched, the document's own configuration prints as before. Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ld#3029) (storytold#3033) text_layer wrote '/OCR BMC BT 3 Tr /PCHelv 1 Tf ... ET EMC'. Tr and the font are graphics state, and ET doesn't reset them, so every content stream after the layer on the page inherited 3 Tr: a header or footer added after OCR was in the file but drew invisibly, in PdfCraft and in MuPDF. The layer is now wrapped in q ... Q inside its marked content. Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…eadline test flake) (storytold#3165) * Control channel: don't lose the refusal reply to a TCP reset When the control channel refused an unauthenticated connection (deadline passed, overlong line, bad first request) it wrote the error line and closed the socket at once. If the client had sent input the server had not read, Linux answers that close with a reset instead of a FIN, and the reset can discard the reply before the client reads it. That is the flake in an_unauthenticated_connection_is_closed_at_the_deadline: the dripping client's bytes were still unread when the server closed. Now the server shuts down its write side after the reply and reads and discards input until the client closes or a 1 s linger passes, and only then closes. The new test a_client_refused_with_input_unread_gets_the_reason pins it deterministically. * Control channel: refused connections linger outside the auth places A connection refused before auth used to keep its place among max_unauthenticated while it lingered (up to 1 s), so refusals could keep real clients out. Now `refuse` gives that place back and takes one of a separate, smaller set of lingering places (Limits::max_lingering, 8) before the reply is written. When none is free the connection is closed right after the FIN instead of lingering, so threads stay bounded as storytold#1806 intended. Tests: a_lingering_connection_holds_no_place_to_authenticate and only_so_many_refused_connections_linger. * Control channel tests: a 5 s linger for the two linger tests Their timing checks (the auth round trips finishing within the linger, and a non-lingering connection failing a write within half of it) had only 1 s and 500 ms of margin with TEST_LIMITS. Each now serves with its own Limits and a 5 s linger; the assertions are unchanged and neither test waits for the linger on its passing path.
storytold#3017) The Hand tool scrolled by the page widget's drag delta from inside the page view's ScrollArea. egui applies that delta when the area ends, after the frame's pages are laid out and drawn, so the page showed each move a frame late and trailed behind the pointer. (Until the page view stopped animating its scrolls, the delta was also eased in over several frames, which is the stutter storytold#924 and storytold#1992 describe.) On top of that, the points the pointer moved before egui tells a drag from a click were dropped, so the page never caught up with the grab point. Pan before the area lays the pages out instead: when the page widget is being dragged with the Hand tool, set the scroll offset from the last offset minus this frame's pointer delta, or minus everything moved since the press on the frame the drag is recognised. The page now moves point for point with the pointer in the frame it moves, and still stops where it is let go (no fling). Other scrolls (go to page, links, zoom anchors, compare sync) are untouched and still win when asked for in the same frame.
…ld#2954) (storytold#2998) On wasm, detect_system_lang() always returned English, so Auto showed English whatever the browser's language. It now reads navigator.languages in order, then navigator.language, and takes the first tag with a catalog, as the desktop builds do with the system languages and as VectorCraft and FilmCraft do in their browser builds. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…told#2979) Found while testing crates/automation/src/content.rs:141 Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
…#2951) Fixes storytold#2944 Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
…rytold#2942) (storytold#2947) Field detection read a path as a rectangle only when it had exactly four corners, and gave up on a subpath once it reached five points. The French income tax form 2042 draws its letter cells with a midpoint on each vertical side (six points), so most of its cells were not found. A subpath now keeps up to 16 points, and a point lying on a straight side between its neighbours is dropped before the four-corner test. A point that turns back along a side is not between its neighbours and stays, so a path that doubles back is still not a box. Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ytold#2736) Fixes storytold#2667 Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
…ytold#2659) (storytold#2697) * M8: redaction removes the replies of the annotations it removes (storytold#2659) A reply (/IRT) placed away from the mark survived, and its /IRT kept the removed annotation - its comment text and the QuadPoints of the redacted words - in the saved file. Replies to a removed annotation, and replies to those, now go with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz * M8: test replies listed before their parent, with a pop-up on a reply (storytold#2659) Review of storytold#2697: the replies precede the annotation they answer in /Annots, deepest first, and a reply has a pop-up. A single pass in /Annots order would keep them; the test fails with the loop cut to one pass, which the first test does not notice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…d#2504) The engine caches a page's text blocks for opening a paragraph and applying its edit, but only while the document is unmodified: it was keyed by the source identity, which edits keep. After the first edit the cache never hit again, and the page was grouped again on every call (the inline editor and the panel each call it after an apply). pdfcraft_cos::Document::read_state() returns what the document reads as: its source identity, its trailer and every changed object, compared as allocations that the state keeps alive. A clone or a snapshot restored by undo has the same state; any set, add, free or trailer change makes a different one. The cache is keyed by it, so it hits after edits and never returns another state's blocks. On a page of 120 rows, text_blocks after an edit took 0.215 ms per call and now 0.006 ms (release build, best of three; a throwaway timing test). Tests: a read state matches a clone and not a document changed by set, free or a trailer change, nor another document; after an edit the engine groups the page once and undo and redo get their own blocks. The engine test fails without this change. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
…orytold#2476) Moving an image whose crop is its own (`q re W n cm Do Q`) as part of a group put a fresh `cm` before the clip path on every move, so twenty moves left twenty `cm` operators in front of one `re`. It never nested deeper, but the content stream grew with every drag. When a `cm` already sits right before that clip (one an earlier move added, or the file's own), `translate_images` now replaces it with the product of the new and old matrices. The CTM at the clip is `c · prev · outer` either way (ISO 32000-1 8.3.4, 8.4.4). A previous `cm` that is not finite, or a product larger than 1e12, keeps the old behaviour and inserts a separate `cm`. The new test moves an image cropped smaller than itself twenty times and checks that the image and its visible crop both moved by the full offset, that the operator count stays at its value after the first move, and that the caption after it is untouched. It fails without this change (32 operators against 13). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…torytold#2346) * fix(engine): a new or recovered document stays unsaved through undo storytold#1435 made undo back to the last saved bytes clean. For a document created by Combine, Extract or Create, or restored from recovery, those bytes were never written where the document belongs, so edit then Undo made it clean: Close no longer asked to save and autosave stopped keeping it. Editor::unsaved_file, set by open_new and mark_recovered and cleared by mark_saved, keeps such a document unsaved until it is saved; Revert follows the same rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz * fix(engine): test that doc_close refuses a recovered or new document after undo Review follow-up on storytold#2346: a recovered document and a new one, rotated and undone, are refused by doc_close without discard_changes, and close with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
…old#2130) (storytold#2232) Find and the renderer place glyphs of a simple font that has no /Widths with the embedded font program's own advances, but redaction guessed the widths from the font's name (a standard-14 face, Helvetica by default). When the guess was wider than the real font, the end of a line under a mark seemed to lie past it and was kept in the content stream, and the verify pass, using the same guess, agreed. Metrics now fills the width table from an embedded sfnt program (FontFile2, or OpenType FontFile3) when /Widths is missing, looking the glyph up as the renderer does. A subset's unmapped codes get width 0 so has_glyph stays false for them. Fonts with /Widths, and bare CFF or Type 1 programs, are unchanged. Fixes storytold#2130
… found by its header (storytold#2398) * Issue storytold#2367: an object stream whose entry cannot place it is found by its header qpdf's xref-compressed-in-compressed.pdf marks object stream 69 as itself compressed inside stream 81, which the format forbids and which does not exist. objstm() loaded the container through its cross-reference entry only, so every object inside (the outlines, the page labels) read as null and a full save wrote `/Outlines null /PageLabels null`, while an incremental save (which copies the original bytes) kept them, and MuPDF recovers them. The container is now found by its own `N G obj` header when its entry cannot load it (scan_for, the same last resort a plain object with a wrong offset has). Regression test: document::tests::an_object_stream_marked_as_compressed_is_found_by_its_header. Verified on the fixture: objects 70 and 82 load (Count 6; 12 page-label ranges) and a full save keeps both. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011K1peyrh5MdZ6ttHkVq8Gh The fallback is for file entries only: once the stream is freed or replaced in the overlay, its objects must not come back from the file's copy (sign's object_stream_changes_cannot_reuse_old_candidate_membership caught this after the rebase); a cos test covers both cases. * cos: a container the saved table marks free is not found by its header either Freeing an object stream, saving incrementally and reopening left the overlay empty and the table saying the stream is free; the header fallback then rescanned the file and the stream's objects came back on the next full save. The fallback now also stops at an on-disk free entry. * M1: put the storytold#2367 test's description on the test, not its fixture --------- Co-authored-by: Brandon Thomas <echelon@gmail.com>
Organize pages ▸ More ▸ Reverse pages (and the command palette) reverses the selected pages among their own positions, or the whole document when fewer than two pages are selected. It is one undoable step, needs the assemble permission like the other page edits, and is available headless as the page_reverse automation tool (pages optional, default all). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M6: detect a row of comb boxes as one comb text field (fixes storytold#1887) * M6: keep empty tables of wide cells out of comb detection (storytold#1887) --------- Co-authored-by: Brandon Thomas <echelon@gmail.com>
WSCAD (and other CAD exporters) write `/ToUnicode /Identity-H` — a name, not a CMap stream — on composite fonts whose two-byte codes are the UTF-16 code units. Acrobat, MuPDF and pdf.js read each code as its Unicode value. hayro resolved the name to the Identity CMap, which maps codes to CIDs only, so such pages had no text at all: nothing could be selected, searched, copied or edited (147 of 147 pages of a WSCAD schematic). - vendored hayro-interpret patch 35: `Type0Font::identity_to_unicode` - crates/fonts: the same reading for Edit text, find and redaction (controls and lone surrogates map to nothing; simple fonts unaffected) Tests fail without the fix: render `identity_to_unicode_names_give_text`, fonts `identity_to_unicode_names_read_codes_as_unicode`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CAD exports draw a whole page as one Form XObject, which Edit a PDF selected
as one page-sized group: no paragraph, line or picture of the page could be
edited on its own. Ungroup (right-click ▸ Ungroup, a double-click on the
group, or `image_edit` action `ungroup`) writes the form's content in place
of its `Do` exactly as ISO 32000-2 §8.10.1 paints it: inside `q … Q`,
through its /Matrix, clipped to its /BBox. The content keeps its bytes; only
resource names that clash with the page's are renamed, open text objects,
marked content and saved states are closed, and /OC is kept as marked
content. The form object and its other placements stay as they are. One
level at a time, one undo step.
Refused with the reason where inlining could change the page: transparency
groups whose blending would change (knockout, transparency inside or
applied to the group, another blending space), forms in the structure tree,
other Default colour spaces, damaged content, content that restores or
closes more than it opens, a Do inside a text object.
The engine test renders the page before and after: pixel-identical. All
147 pages of a real WSCAD schematic ungroup; rendering is identical except
one anti-aliased edge row on 4 A3 pages where the box and an identical clip
of the content meet on a half pixel (at most 7/255).
The picture menu gets its own id scope: its first items took the ids of
the page menu's that share the popup ("Second use of widget ID").
New parity entry edit.form-content-editing (partial); Ungroup and
"Ungroup artwork" in all 22 catalogs (ATTRIBUTION.toml hashes updated).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gaps.md: gap 3 records Ungroup for grouped Form artwork and /ToUnicode /Identity-H read as Unicode as partial progress (in-place form editing, refused groups and vector paths still open); gap 16 records the pre-existing shared picture/page menu popup in Edit text & images (items drawn over each other, a click over another page's picture taken by that page), not fixed. target-app-parity.md: area C and All counts by the one new partial entry edit.form-content-editing; the table still lags the checklist, as noted in its revision history. Estimates unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
My own people.toml entry (real name shown next to the username when the About window's name toggle is on "Real name"), committed by me as docs/contributors.md asks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lcoding15
force-pushed
the
fix/form-ungroup-identity-tounicode
branch
from
October 18, 2026 09:42
6df41e1 to
78e5c89
Compare
Lcoding15
pushed a commit
that referenced
this pull request
Oct 22, 2026
Lcoding15
pushed a commit
that referenced
this pull request
Oct 22, 2026
verify_noting adds the note only when the signature verifies. The test uses a synthetic 1024-bit key and its PKCS #1 v1.5 signature: the valid signature carries the note, and one that doesn't verify has none.
Lcoding15
pushed a commit
that referenced
this pull request
Oct 30, 2026
Merge main; set typing_from in two tests from storytold#4441
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.
What this changes
How you verified it
cargo fmt --check-D warningson the crates I changedcargo xtask layersandcargo xtask assets(if you touched crate boundaries or any asset)Tests. New behaviour needs a test at the right scale — a property test, a unit test, a
crate integration test or an end-to-end one. For a bug fix, please check that the test
fails before your fix by reverting the fix and watching it go red. A test nobody has seen
fail is not evidence that it pins anything.
Never-crash
Every PDF, script, MCP call, settings file and keystroke is untrusted input (
AGENTS.md§4).unwrap/expect/panic!/unreachable!/todo!outside tests, unless it isprovably infallible with a comment saying why
get, and strings only at charboundaries); checked or saturating arithmetic; allocations sized by input are capped;
recursion is bounded
unsafeProvenance
This is the part that cannot be undone after a merge, and the part a new contributor is
least likely to know about. Please read
AGENTS.md§1 before adding any asset.storytold/craft-fonts)
run as external oracle processes
another implementation's source
ATTRIBUTION.tomlentry andcargo xtask assetspassesSecurity
--rootconfinement still holds on every path, including ones reached by nestedtool dispatch
Parity and reach
parity/acrobat-features.tomlupdated in this PR, withnotessaying what is stillmissing —
shippedmeans "exists and tested", not "as good as Acrobat"automationtool where that applies (AGENTS.md§3),so agents can drive it and not just the UI
docs/, crate README)Anything a reviewer should look at first