With Add text the page's own paragraphs took input only while the pointer was over one
(over_source), and their drags end in edit_text_ui::page_input. A paragraph dragged off its box
was left hanging after the release (block_drag), to land wherever the pointer next met a
paragraph; one flicked off in its first move never started, so nothing moved. main has the same
test. Add text now also acts on the paragraph the page router says holds the press, until the
button is up, so the drag ends where it is released, as with Edit text, and Add text's
new-box path does not take the press over.
New test add_text_finishes_a_paragraph_drag_that_leaves_its_box, seen failing before the fix
(the drag still pending after the release); the flick was checked by hand the same way. From
CodeRabbit's third review of #5300.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A click acted on what its press was on wherever the button came up: a press on a link, a push
button, a signature field or a list box that slipped off it before the release (still a click to
egui, within 6 px) followed the link, ran the button's action, opened signing or picked a list
box's option. On main a click went to what lay under its release. Now it needs both: the
link's or the fields' hit test at the release point must give what the press was on, as a button
pressed and released off itself does nothing. Check boxes and radio buttons already did so, by
their overlay's click-outside test.
New tests a_press_on_a_link_released_off_it_follows_nothing and
a_press_on_a_button_released_off_it_runs_nothing, seen failing with this commit's source
reverted; the check box test shares their slipping_click helper. From CodeRabbit's second review
of #5300.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Add text, the page router's hits kept only the paragraphs' bodies (3d14ffd7), so a click
just inside a closed paragraph's left or right edge, within the edge's grip, had no owner: it
neither opened the paragraph (over_source counts the point as the paragraph's) nor started a new
box. Add text now keeps a paragraph's hit wherever the point lies in a paragraph's box, edges
included, by the same test as over_source (edit_text_ui::over_paragraph), and drops it only
beside the box, where a click starts a new box. The edge's grip shows there again, and a drag
from it rewraps the paragraph, as on main.
New test add_text_opens_a_paragraph_from_just_inside_its_edge, seen failing with this commit's
source reverted. From CodeRabbit's second review of #5300.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Add text, the page router's hits included Edit text's pictures and paragraph edges, which
nothing acts on with Add text, and Add text starts a box only where nobody owns the press: on a
scanned page (a picture filling it) no click placed a box, a click within a paragraph's edge
grip beside it did nothing, and the cursor over a picture was the hand. Add text now has only
the paragraphs' bodies, over their text, where it finishes the box being typed (over_source).
New test add_text_places_a_box_on_a_picture_and_beside_a_paragraph, seen failing on the
previous commit. From CodeRabbit's review of #5300.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From a review of this branch:
- Escape mid-drag with a text markup tool (Highlight, Underline, Strikeout, Squiggly, Replace
Text) or Redact over text left the half-made selection on the page: the cancel took the Escape
that would have cleared it. It is dropped now. The marks made from a selection at its end
(after the page tools, which the release blocking did not reach) also wait for a cancelled
press's button; egui ends its own drag on Escape without a drag stop, so they were not made
in practice, but nothing but egui stopped them.
- The open paragraph's frame took the press above and below the text only: its sides still gave
edge grips (a closed paragraph's rewrap handles) on the Editor layer, over the frame. Edges now
belong to closed paragraphs only.
- A press in the frame right after a release kept the last press's owner until a page claimed
it, so a drag from the margin beside a page could move the comment just clicked. A new press
now drops the last owner before the pages give it theirs.
- A release and a new press in one frame no longer leave the new press cancelled.
- Added content is picked up with the tools that act on it (Select, Add text, Edit text), not with
every tool while Edit a PDF is open.
- The layer docs in page_cursor.rs say what lies on each layer now.
New tests an_open_paragraphs_frame_takes_the_box_beside_its_text and
a_drag_from_beside_the_page_right_after_a_click_leaves_the_comment_alone (the release and the
next press in one frame) fail on the previous commit. escape_cancels_a_highlight_being_dragged_over_text
(with a control: the drag highlights without Escape) passes there too, for the reason above, and
stays as a guard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A "Page presses" non-negotiable in CLAUDE.md, beside "Page input" and "Page cursor": a page
module decides what of its own is under a point in one hit test (a Hit from
canvas/page_router.rs) that gives its cursor, its tip and its press, and acts on a press only
when the router says it is its own, never by testing the press point again or by asking
another module; Escape cancels any press the pages hold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit text's paragraph edges were tested twice: by edit_text_ui for the cursor, on the page's
paragraphs, and by object_ui for the press, on its own object inventory (grabs_edge). Where the
two inventories disagree, the resize arrow shows and a press starts an invisible area selection
instead (#2276, the review's step-3 probe). The open paragraph's frame showed the Move cursor
4 px out and took the press 6 px out.
Each of these parts now has one hit test, in the page router's table, and acts only on the
press it was captured on:
- edit_text_ui::hit: a paragraph's edges (Handle), its body (Text; Editor for the open one).
- edit_text_ui::picture_hit and picture_handle_hit: a picture's body on the Text layer, asked
before the paragraphs, so text on top of artwork takes the press; the selected picture's
corners and rotate knob on Handle, asked after them, so they win over text.
- object_ui::hit: the objects selected together (Placed) and the open paragraph's frame
(Handle), with one reach (FRAME_REACH, 6 px) for the cursor and the press. An area selection
starts only where nobody's hit claims the press.
- content_ui::hit: added text and pictures (Placed; the selected one's corners on Handle), over
form fields; Add text starts a new box only where nobody's hit claims the press.
image_owns_pointer and content_ui::select_grabs go, with the edge test object_ui kept a copy of
(grabs_edge). The page's hits move into canvas::PageHits.
Un-ignores a_paragraph_edge_resizes_wherever_it_shows_the_resize_cursor: it passes on main
already (the inventories agree for its paragraph since the probe was written), so it stays as a
guard rather than as proof. New test an_open_paragraphs_frame_shows_move_wherever_a_press_takes_the_box
fails on the previous commit (the cursor is Default there).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Forms: forms_ui::hit is the fields' only hit test (cursor and press). The press opens a text
field (#2213) and its click acts on the field the press was on, from the page router, instead
of the field under the release point: a press on one check box released over the one beside it
ticked the second. Now it ticks neither, as a button pressed and released off itself does
nothing. Forms no longer wait on comments::select_grabs: a comment that moves (a note, a shape,
a signature, a Fill & Sign mark) hits on the Placed layer, over form fields, so it still takes a
press over a field (a signature can be dragged off the field it covers), and text markup stays
on the Comment layer, under them, so a click on a box under a highlight ticks the box.
select_grabs goes.
Prepare Form: prepare::hit (the selected field's corner handles, then a field, on Placed so a
field being moved stays over the comments as before) gives the cursor, and the drag, click and
double click act on what the press was on; a press held by something else is not Prepare's.
New tests: a_press_on_one_check_box_released_over_another_ticks_neither (fails without this
commit: the second box is ticked) and a_check_box_under_a_highlight_takes_the_click (fails with
markup on the Placed layer).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Sign area, marquee zoom and snapshot, crop and redaction boxes, and the stamps, act where
nothing drawn over the page holds the press (the page router has no owner for it), rather than
wherever the press lands on the page. Fill & Sign already asks the router (fill_grabs).
Their drags start on the primary button and end with its release, however egui reported it:
its drag stop, or the button found up. They started on any button, so a right-drag drew a
signature rectangle and opened the signing dialog (and drew a crop or a marquee), and the end
waited for a pointer position, so a drag whose release came without one never ended.
New test a_right_drag_draws_no_signature_rectangle, seen failing with the drag started on any
button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page router (canvas/page_router.rs): a page module answers "what of mine is under this
point?" in one hit test, which gives a Hit (an owner, a layer, a cursor, a tip). On the frame of
a press, the hits at the press point decide who it belongs to, by PR 2's table (the highest
layer the tool reaches), and the router holds that capture until the button is up and its
release has been seen; automation's cancel and Escape drop it. The pointer's hits claim the
cursor and the tip, after the tools that are not converted yet, as before.
Comments: comments::hit is the module's only hit test, with each tool's filter (Select: point
handles, resize handles, then the comment; Fill & Sign: the same over its marks only; shape
tools: the selected comment's handles only). The Select tool's drag, click and double click act
on the comment and part the press was captured on, instead of testing the press origin and the
release point again; fill_grabs and select_grabs ask the router while the button is down. The
topmost comment is the one drawn last (/Annots order), not the last in inspection's top-edge
order: over stacked comments the tip named the top one while a click selected the one
underneath. Text markup declines the drag, so a click on a highlight selects it and a drag
across it still selects the text.
Links: the link hover is a hit (hand and target), and a click follows the link its press was
on. Text selection starts unless the press is held by something that keeps its drag (a link, a
comment that moves); it tested for a link under the pointer at the moment egui called the press
a drag, so a drag from text onto a link selected nothing.
Un-ignores a_click_on_stacked_comments_selects_the_one_drawn_on_top; new test
a_drag_from_text_onto_a_link_selects_the_text. Both fail with their fix reverted (inspection
order for the topmost comment; the link test at the pointer).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Escape cancelled a comment gesture, Prepare's marquee, measuring and the camera tool, each in its
own module; with the Sign area, crop, marquee zoom, a redaction box, the Link tool's rectangle,
a paragraph or picture drag, added content, an object move or a text selection it did nothing,
and the release then drew, placed or opened what the drag had begun (the step-4 probe: Escape
mid-rectangle still opened the signing dialog).
document_area now handles Escape once, before the pages: while the pages hold a press, it drops
every pointer gesture (DocView::cancel_pointer_gestures, as automation's cancel does) and marks
the press cancelled. Until the button is up the page tools take no input from it, so its
release neither clicks (a stamp, a Fill & Sign mark) nor ends a drag. Escape without a press
keeps its meanings (close the editor, clear the selection, back to the Select tool).
Un-ignores escape_cancels_a_signature_rectangle. Two new tests: Escape mid-press keeps the
release from placing a stamp, and Escape mid-drag abandons a crop rectangle. All three fail
with the cancel taken out; the stamp test also fails with the cancel kept and the release let
through.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #5228. The finished CMYK group was drawn through
whatever paint transform a preceding shading pattern left on the
context, so its cyan half showed magenta. Hayro patch (27) now resets
the paint transform before drawing the group's pixels.
Two regression tests, both reported in that review:
- a pattern painted before the group (fails without the reset);
- the Color blend mode over an empty CMYK group, swept over all 256 K
levels (failed on nine levels before #5199's vello_cpu fix was merged
in; passes with it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Resolves test-file conflicts where main and this branch both appended tests:
main's file plus this branch's appended tests, unchanged.
fmt, clippy -p pdfcraft-forms -p pdfcraft-automation, and both crates' tests pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Resolves test-file conflicts where main and this branch both appended tests:
main's file plus this branch's appended tests, unchanged.
fmt, clippy -p pdfcraft-forms -p pdfcraft-automation, and both crates' tests pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Resolves test-file conflicts where main and this branch both appended tests:
main's file plus this branch's appended tests, unchanged.
fmt, clippy -p pdfcraft-forms -p pdfcraft-automation, and both crates' tests pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
main's knockout patch (hayro (22)) draws each element of a knockout group
once in erase mode, to clear what the group drew under the element's shape.
An isolated DeviceCMYK group nested in such an element still took the CMYK
path in that pass, and drew its finished pixels with Normal blending over
what should have been cleared. draw_isolated_cmyk_group now declines while
erasing, so the group's contents are interpreted in erase mode as before.
The new test fails without the fix (the centre shows red through the
magenta: [240, 63, 168] against [245, 127, 197] for the DeviceRGB control).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
main has taken hayro-interpret patches up to (66) and hayro up to (25)
since #3777 was opened, and open PRs claim hayro-interpret (67)-(69) and
hayro (26). The isolated DeviceCMYK group patches become hayro-interpret
(70) and hayro (27). vendor/README.md names the tests by their new path,
raster::tests::isolated_cmyk_groups.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Review of #3777 (test report by mtlcl410).
- A group wider than about 512 device pixels lost its blend mode in the
tiles that a paint covers completely: on the bare pass canvas, vello_cpu
fills such a tile without the blend mode. At 300 dpi, magenta then black
with Difference drew black in the middle of the group, not green. Each
ink pass now draws inside a layer of its own.
- A nested isolated group that names another blending space (for example
/CS /DeviceRGB) blends in that space (ISO 32000-2 §11.3.4). The
interpreter now tells the device about such a group
(Device::group_blends_outside_device_cmyk), and a pass that meets one
declines, so the enclosing group is drawn as before. A non-isolated
group's /CS is ignored (ISO 32000-2 §11.6.6, Table 145, CS: it takes its parent's space), so
it stays in the enclosing group's CMYK.
Test: raster::tests::isolated_cmyk_groups_blend_at_any_size_and_respect_nested_spaces.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #3777. `Color::ink_tints` no longer gives tints for a
four-component ICCBased colour. Such a colour keeps its own profile, so a
group that holds one is drawn as before. vendor/README.md: the hayro
patch (21) entry refers to hayro-interpret patch (45), not (44).
ISO 32000-2 §11.3.4: the elements of a transparency group blend in the
group's colour space. For a subtractive space, each component is
complemented before the blend function and again after it. §11.3.5.3:
the nonseparable modes blend C, M and Y as their complements R, G and B,
and K comes from the backdrop (Hue, Saturation, Color) or from the source
(Luminosity). PdfCraft converted each colour to RGB and blended in RGB.
A print test file's blend-mode patch then showed an X in almost every
cell: magenta under black with Difference stayed magenta, not green.
hayro-interpret patch (45): CmykGroup and Device::draw_isolated_cmyk_group
(default false) for isolated, non-knockout groups with /CS /DeviceCMYK.
interpret_form_contents is now a function of its own. Color::ink_tints
and color::cmyk_to_srgb8 are new.
hayro patch (21): the renderer draws such a group as a CMY pass and a K
pass of complemented tints. It converts the tints to RGB with the CMYK
profile and composites the result through the group's layer. The group
is drawn as before with reading colours, for contents without process
tints (RGB, spot colours, patterns, images) and above 2^24 pixels.
Test: raster::tests::isolated_groups_blend_in_device_cmyk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M5: Import comments takes another PDF and copies its comments onto the same pages
Import comments read only XFDF, FDF, XML, XDP, CSV and text data files.
It now also takes a PDF: each comment of that PDF is copied onto the
page with the same number, at the same place, with its appearance
stream, its pop-up, its replies and its layer. Comments on pages past
the document's last page are left out, together with replies to them.
Links, form fields and redaction marks are not copied. A comment named
like one already on its page replaces it, as an XFDF or FDF import
does. A PDF without comments is refused with "nothing in the file
matches this document", and a PDF that needs a password is refused.
- organize: import_comments copies the comments with the page copier
that Insert Pages uses, and registers the layers they use.
- engine: Edit::ImportData reads a file that starts with %PDF- as a PDF.
Any other file goes to the data importer first, and is read as a PDF
when that fails, %PDF- occurs in its first 1,024 bytes and the PDF
reader opens it.
- ui: Comments > Import comments lists .pdf files; form.import_data
lists the same files as before.
- automation: the doc_import_data description names the PDF case.
- parity: comment.import-from-pdf is shipped.
* Import comments: an annotation that is no comment keeps its name
A comment imported from a PDF replaced any annotation of the same /NM
on its page that was not locked, a redaction mark or a widget, so a
link, screen or 3D annotation of that name was overwritten. Only an
annotation whose subtype is one of the copied comment subtypes is
replaced now; for any other, the copy is added beside it without the
name. The subtype is read through an indirect reference.
* Import comments: index each page's annotations instead of scanning them per comment
* Import comments: a layer that BaseState OFF hides stays hidden
* Import comments: a PDF with binary bytes before its header imports on an XFA form, and two comments with one name both land
On an XFA form, a decode failure no longer stops the import when a PDF
header is in the first 1,024 bytes; the PDF's comments are copied. Two
incoming comments on one page with the same /NM no longer replace the
same comment: the first replaces it, and the second is added without
the name.
* Import comments: an inline annotation on the page is replaced by name
An annotation written inline in a page's /Annots is made an indirect
object before names are matched, so an incoming comment with its /NM
replaces it instead of being added beside it.
* Import comments: two new comments with one name keep it once
When no comment on the page has the name, the first incoming comment
keeps its /NM and each later one with the same name is added without it.
* M6: Organize pages ▸ Page properties sets the selected pages' tab order
Page properties in the Organize pages panel was listed as planned. In
the app, a page's tab order could only be set for every page at once,
with the form.tab_order commands, which offer no Unspecified. (The
form_tab_order tool already set chosen pages, Unspecified included.)
- Page properties (page.properties) opens a Tab Order dialog for the
selected pages, with Use Row Order, Use Column Order, Use Document
Structure and Unspecified. OK writes Edit::SetTabOrder for those pages
as one undo step. The dialog opens on the pages' order, or with no
choice made when they differ, and then OK is off until one is chosen.
OK on the order the pages already have adds no undo step.
- Rows and columns run right to left in a document bound on the right,
as Use Row Order and Use Column Order are described. PdfCraft reads a
right-to-left binding from /ViewerPreferences /Direction /R2L
(ISO 32000-1, 12.2, Table 150). Before, rank_tabs ranked them left to
right.
- A page whose /Tabs is an indirect object now takes its row, column or
structure order. Before, rank_tabs read only a direct name and ranked
the page in annotation order.
- pdfcraft_forms::tab_orders and Document::tab_orders read each page's
/Tabs. form_tab_order returns them as page_tabs, and reads without
changing anything when it is given no order or field.
- set_option("dialog", "page-properties") opens the dialog.
- Parity: organize.page-tab-order is shipped, and its notes say the
Actions tab of Page Properties (organize.page-actions) is still
planned; the Organize pages grid notes list Page properties.
* Page properties: OK applies only to the document the dialog was opened on
The draft keeps the document's id. When another document is active by
OK, the dialog closes without changing either document.
* Menus: list Page properties… in the Pages menu
The menu bar now lists its commands explicitly, and every command with a menu group must appear in it.
* Page properties: apply the tab order only to an unchanged document
An edit, undo or redo made while the dialog is open can move the pages
at the stored indices. OK now closes the dialog without changing a page
when the document's edit generation differs from when it opened.
* Page properties: say when a document change cancels the tab order
OK shows a notice when the document changed or another document became
active while the dialog was open, so the user knows to open it again.
* M6: form script messages keep their title, icon and buttons, and every one is shown (#4293, #4294)
* M6: a form script message waits for a notice the toast already shows
* M6: form script messages past the queue's room are counted and reported
* M6: a sentence break in the merged parity note
* M6: an object that names no parameter is a message's text, as a String object or a caught error
* M6: the count of left-out script messages is said again if another notice replaces it before its time
* M6: a full stop between two sentences of the FormCalc parity note
* chore: re-run CI (a frame-budget timing test missed on a shared runner)
Export Data (CSV) wrote field names and values as they are, so a value
from a received form that starts with = or @ (or + or - when it is not a
number) runs as a formula when the CSV is opened in a spreadsheet.
It now uses the rule merge_csv got in #4101, moved unchanged into
runs_as_formula, so the merge report's output is the same. Import Data
takes the added ' off again, so Export Data then Import Data gives back
every value unchanged.
The script already validated CARGO_BUILD_TARGET and named the archive after the arch, but
cargo build ignored it, so a cross build compiled for the host. Pass --target, resolve
host-tuple to the build host's triple first, and skip the --version smoke run for any
CARGO_BUILD_TARGET build (an aarch64 binary cannot run on the x86_64 VM).