Refs #5989. Ctrl/Cmd+A selects every object on the page; copied objects paste as page content that draws the same, within a document or into another, with organize's bounded deep copy across documents. Headless: object_copy, object_paste, object_move all.
7.8 KiB
Move several objects together
In Edit a PDF → Edit text & images, select several items on the same page:
- Shift-click, Ctrl-click, or Command-click toggles an item. An ordinary selected paragraph, image, or added item becomes the first member when you extend its selection.
- Drag through empty page space to draw a selection rectangle. Items intersecting the rectangle are selected; hold one of those modifiers to add to the selection.
- Drag the selection to move all its items by the same distance. Blue outlines preview the move; releasing commits it as one undo step. Escape cancels the gesture or clears the selection.
- Switching tabs, opening Home or a modal dialog, or losing window focus cancels a drag preview. Returning to the document keeps the selection without committing a release from elsewhere.
- Modifier-clicks prefer added items and text over background artwork, so labels remain selectable.
- Clear the selection or double-click an item to return to ordinary text editing and image resizing. Finish or discard a text draft before starting a multi-selection.
- ⌘A / Ctrl+A selects every item on the current page (paragraphs, artwork and added items), as Acrobat's Edit PDF does, instead of the page's text. Inside a paragraph or text box open for typing it still selects that text.
Copy, cut and paste
With items selected together, ⌘C / Ctrl+C (Edit ▸ Copy) copies them and ⌘X / Ctrl+X (Edit ▸ Cut) copies them, then deletes them as one undo step. ⌘V / Ctrl+V (Edit ▸ Paste) pastes them onto the current page of any open document, also after the original changed or closed, as one undo step, and selects what it pasted. Onto the page they came from they land 18 points down and to the right, as Acrobat offsets a paste onto the same page; elsewhere where they were; each further paste onto a page steps another 18 points; a copy that would land off the page is centred.
A paste draws the same: it copies the items' content operators with the graphics state they were drawn in (transformation, clip, colours, transparency, text state) and every resource they use (fonts, images and Form artwork, ExtGStates, colour spaces, patterns, shadings), merged into the target page's resources under names that don't clash, as Ungroup merges them. Within a document the same font and image objects are drawn again. Into another document they are copied with what they refer to, each once whatever the cycles, sharing objects it already holds; a paste whose resources take more than 20,000 objects or 256 MB is refused with that reason. Pasted items are ordinary page content: an added text box comes as page text, untagged and in no layer. The system clipboard gets the copied text (or, without text, the first picture) for other applications; a newer copy there, here or in another application, wins ⌘V. Text drawn as a clipping path before the selection is refused, as is artwork without resources of its own that names one, where a paste would change it.
The supported items are the existing paragraph boxes, raster Image XObjects, whole Form artwork, and text/images added by PdfCraft. A Form remains one item, including its nested artwork and resources. Added items appear once in the selection inventory. Selections stay on one page and contain at most 1,000 items; pages with more than 10,000 recognised objects are refused for group selection. Content streams must decode completely within 64 MiB each and 128 MiB per page, with a 1,024-level graphics-state limit and 100,000 content operations. These limits apply to both the original page and the moved result; unreadable or missing streams are refused. Moving between pages, arbitrary paths, inline images, group resizing, align/distribute, and vertical or clipping text are outside this operation's scope. A refusal changes nothing.
Added streams move by changing their outer placement, preserving drawing bytes, fonts, crop paths,
and image transforms even if the page was rotated after the item was added. Their original local
coordinate basis is retained in /PCAdded/MoveOrigin so metadata and source placement agree after
repeated moves. Ordinary content updates replace that stream and reset the basis.
Existing text moves without retyping, font substitution, or reflow: its original encoded glyph
strings and kerning are replayed with temporary numeric TJ adjustments and text rise. The reader
performs the original glyph advances, so following unselected text keeps its position. Paragraph
indices can change when detection groups lines differently after a move. The UI remaps its own
selection only when the new geometry matches uniquely; other document edits clear stale selection.
Engine
Document::editable_objects(page) returns a mixed inventory with zero-based ObjectTarget
references and user-space rectangles. Apply Edit::MoveObjects { page, objects, offset } through
Session. Targets resolve together before mutation. Permission checks, dirty state, history,
incremental saving, and refreshing use the same transaction as other edits.
Document::object_move_offset(page, [dx, dy]) converts a displayed-page vector (points, right/down)
to the user-space vector required by the edit, accounting for the crop box, /Rotate, and /UserUnit.
A frontend with an additional view rotation converts its pointer positions to displayed page space
first. Duplicate, missing, non-finite, oversized, or unsupported targets fail atomically. Engine callers
re-list after changes; the automation generation guard detects stale references before the edit.
CLI and MCP
Both frontends use the same tools. object_list returns a document generation and items with
kind (added, text, or image), 1-based index, type, displayed rect, and optional text.
Treat those references as belonging to that generation and pass it back when moving:
[
{"tool":"doc_open","args":{"path":"input.pdf"}},
{"tool":"object_list","args":{"doc":1,"page":1}}
]
After choosing references from the returned list, use a move call like this in the same MCP
session. For a CLI script, include doc_open in that script before the move:
[
{"tool":"object_move","args":{
"doc":1,"page":1,"generation":0,
"objects":[{"kind":"text","index":1},{"kind":"image","index":1}],
"offset":[24,18]
}},
{"tool":"doc_save","args":{"doc":1,"path":"moved.pdf"}}
]
{"all": true} instead of objects takes every item on the page, as ⌘A does. object_copy takes
the same references (with cut: true it deletes them afterwards, one undo step) and object_paste {doc, page} pastes them, into the same or another open document, as the app does:
[
{"tool":"object_copy","args":{"doc":1,"page":1,"all":true}},
{"tool":"object_paste","args":{"doc":2,"page":3}}
]
Use the actual generation and references from your document, rather than assuming these example
values. offset is in displayed points: positive x moves right, positive y moves down. The optional
generation guard rejects a changed document before interpreting references. Re-list after any edit,
undo, redo, or save; there is no claim that an index persists across generations. Run a script with
pdfcraft-cli run --script steps.json --root DIR or call these tools through the opt-in MCP server.
Regression coverage: crates/edit/src/tests.rs (glyph bytes, neighbours, split streams, shared Forms,
rotated/cropped/UserUnit pages and refusal), crates/edit/src/objects/clip_tests.rs (copies that
render the same pixels, renamed resources, hostile content), crates/engine/src/tests.rs and
crates/engine/src/object_clip_tests.rs (permissions, history, rendering, save/reopen, pastes into
another document, cycles and the size limit), crates/automation/tests/it/automation.rs (generation
guards, hostile references, copy/cut/paste tools), and crates/ui-egui/tests/it/object_selection.rs
(real-shell selection, gestures, ⌘A and the clipboard keys).