Files
pdfcraft/crates/engine/src/edit.rs
T
Michael Higgins 4efcfa6ea3 fix(crypt, engine): refuse page extraction when Protect allows any change except extracting pages (#5081)
## What this changes

Adds `Permissions::extract_pages()`: `assemble()` **and** copying (bit 5), as #5081 asks, since extracted pages carry their text and graphics into a file without the source's protection. It is used for Extract, Split, Split by size and every Combine/Insert/Replace/paste source check (`sources::source_document`). Changes within the document (rotate, delete, move, insert) keep `assemble()`.

Since this PR was opened, #6309 made `assemble()` revision-aware on `main`: bit 11 alone at revision 3 and later, bit 4 at revision 2. That already refuses "Any except extracting pages" at revision 3+. This PR builds on it:

- At revision 2, `assemble()` reads bit 4, which "Any except extracting pages" sets, so such a file could still be extracted and split into unprotected copies. Requiring copying closes that.
- New `Needs::PageExtraction` greys out Extract and Split up front. It reuses the existing security-refusal string, so no catalogue changes.
- `Document::allows_page_extraction()`, and an `extract_pages` key in the automation security JSON.
- `page_extract` with `separate: true` extracts and writes one page at a time (`extract_separate`), so it no longer buffers every page. The first page is extracted before `out_dir` is created, so a refused extraction leaves no empty directory. If a later page fails, the files this call wrote are removed, and `out_dir` too if this call created it (review on this PR).
- Insert Pages checks a PDF through the same path as the insert itself: new `Session::insert_source(dest, name, bytes)`, which the UI's insert now calls (review on this PR).
- The `organize.combine` parity notes now say "page extraction".

Fixes #5081

## How you verified it

`cargo fmt --all` is clean. Regression tests (adopted from Harrison-Booth's review patch, plus CodeRabbit's follow-ups):

- `crates/crypt/src/tests.rs::page_extraction_needs_assembly_and_copying`: unit test for `extract_pages()`.
- `crates/engine/src/tests.rs::extraction_permissions_cover_revision_three_to_six_and_leave_no_partial_output`: AnyExceptExtract on RC4-128, AES-128 and AES-256 refuses Extract, Split, Split by size and Combine/Insert/Replace sources without the permissions password, leaves source and destination untouched, and the owner can do everything. The authorized combine output has 2 pages, and a differently encrypted or unprotected destination is refused.
- `crates/engine/src/tests.rs::page_extraction_needs_the_copy_permission_on_every_revision`: a file that withholds only copying (R2 RC4-40 and AES-256) refuses extraction; the owner password lifts it.
- `crates/engine/src/tests.rs::pages_under_the_same_protection_are_inserted_without_page_extraction`: a separately built file under matching protection is inserted, and the result stays restricted.
- `crates/automation/tests/it/automation.rs::refused_extraction_writes_no_split_or_page_output`, and the `extract_pages` checks in `protecting_through_tools`, which now cover three presets, including a user (not the owner) for whom extraction is allowed.

**Only the crypt unit test has been run locally, and it passes.** The engine and automation tests timed out while compiling on this machine and have not been run, nor seen failing without the fix. The one `pdfcraft-ui-egui` call-site change has not been compiled. CI is the first place these build.

- [x] `cargo fmt --check`
- [ ] clippy with `-D warnings` on the crates I changed (not re-run after the rebase)
- [ ] tests pass on the crates I changed (crypt only)

## Anything a reviewer should look at first

- **Rebased onto #6309.** The tests no longer expect "Any except extracting pages" to allow assembly at revision 3+. The same-protection paste tests now use the "Inserting, deleting and rotating pages" preset without copying.
- **Copy (bit 5) is now required**, as the issue suggests. `crates/ui-egui/tests/it/editing.rs::protected_files_that_can_be_combined_are_flagged` used a copy-withheld file as "protected but combinable"; it now uses a print-withheld file, since a no-copy file is no longer a Combine source without the permissions password.
- **Same-document paste**: Paste re-opens the document's own bytes as an Insert source, which the new check would refuse. `sources::open_source_into` accepts a source that withholds extraction but allows assembly only when the destination will be saved under exactly the same protection (file key and `/Encrypt` values), so cut/copy and paste within one protected document still works. Pasting into an unprotected or differently protected document is refused. One limit: after changing security in the same session without saving, a cut can't be pasted back until you save.
- **Not in this PR:** CodeRabbit's `disabled_reason` suggestion for read-only documents predates this change and covers other commands too, so it belongs in a separate UI change.


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

- **New Features**
  - Page extraction and splitting now follow a dedicated permission check, distinct from page assembly.
  - Pages can be inserted from certain protected PDFs when their protection settings match the destination document.

- **Bug Fixes**
  - Separate-page extraction now cleans up files created during a failed operation and avoids creating an output directory when the first page cannot be extracted.
  - Security details now report whether page extraction is permitted.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-11 09:06:48 +01:00

688 lines
24 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The edits a document takes: what each is called, whether its permissions allow it, and running it.
use std::sync::Arc;
use crate::{
AddedContent, AddedText, Background, BorderShape, BoxSpec, EditError, FieldProps, FieldValue, FillMark, Hairlines, HeaderFooter, Hidden,
ImageEdit, InitialView, Label, LineEnding, LinkAction, LinkStyle, MarkFile, MarkKind, Markup, NewAnnotation, NewField, NoteIcon, ObjectTarget,
PageBox, PrinterMarks, Protection, ReviewState, Rgb, Shape, Standard, Style, TabOrder, TransitionSpec, Watermark, a11y, copy_image,
decode_data_file, embed_image, field_table, fit_picture, image_content_type, is_xfa_data_file, js, mark_source, measure, model_poster, strip_xfa,
unrestricted, xfa, xfa_after_data_change,
};
mod labels;
mod run;
pub use labels::{BATCH_LABELS, EDIT_LABELS};
pub(crate) use run::{EditCtx, check_permission, run_edit};
/// Edits that can be applied to a document. Page indices are 0-based.
#[derive(Clone, Debug, PartialEq)]
pub enum Edit {
RotatePages {
pages: Vec<usize>,
degrees: i64,
},
DeletePages {
pages: Vec<usize>,
},
MovePages {
pages: Vec<usize>,
to: usize,
},
/// `pages` reversed among the positions they occupy.
ReversePages {
pages: Vec<usize>,
},
InsertBlankPage {
at: usize,
width: f64,
height: f64,
},
/// Copies of `pages` inserted at `at` (an index in the page order as it is now), or after
/// the last of them when `at` is `None`.
DuplicatePages {
pages: Vec<usize>,
at: Option<usize>,
},
/// Replace Pages: the content of `pages` is replaced by `src_pages` of another PDF; the
/// pages' links, comments and bookmarks stay.
ReplacePages {
pages: Vec<usize>,
name: String,
bytes: Arc<Vec<u8>>,
src_pages: Vec<usize>,
},
/// Set Page Boxes / Crop: set (or reset) one box on `pages`.
SetPageBox {
pages: Vec<usize>,
which: PageBox,
spec: BoxSpec,
},
/// Set or remove the page transition and display time of pages (ISO 32000-2 §12.4.4),
/// Organize ▸ Page Transitions.
SetTransition {
pages: Vec<usize>,
spec: TransitionSpec,
},
SetInfo {
key: String,
value: String,
},
/// Insert pages from another PDF (all pages when `pages` is `None`) at position `at`.
InsertPagesFrom {
name: String,
bytes: Arc<Vec<u8>>,
pages: Option<Vec<usize>>,
at: usize,
/// How a form field whose name the document already uses is kept separate.
field_conflicts: pdfcraft_organize::FieldConflicts,
},
/// Add a bookmark to `page` as child `index` of the bookmark at `parent` (`[]` = top level).
/// It goes to the user-space point (`left`, `top`) of the page at the reader's zoom, and
/// either may be `None` to keep the reader's position that way (both `None` is the page top).
/// A coordinate that is not finite fails the edit.
AddBookmark {
parent: Vec<usize>,
index: usize,
title: String,
page: usize,
left: Option<f64>,
top: Option<f64>,
},
RenameBookmark {
path: Vec<usize>,
title: String,
},
/// Delete a bookmark and the bookmarks under it.
DeleteBookmark {
path: Vec<usize>,
},
/// Move a bookmark to child `index` of `to_parent` (indices after removing it).
MoveBookmark {
from: Vec<usize>,
to_parent: Vec<usize>,
index: usize,
},
/// Point a bookmark at another page.
SetBookmarkPage {
path: Vec<usize>,
page: usize,
},
/// Colour a bookmark's title (`/C`); `None` or black is the default black.
SetBookmarkColor {
path: Vec<usize>,
color: Option<[f64; 3]>,
},
/// Make a bookmark's title bold or italic or not (`/F`); `None` keeps that one.
SetBookmarkStyle {
path: Vec<usize>,
bold: Option<bool>,
italic: Option<bool>,
},
/// Write `bookmarks` as children of `parent` from child `index` on, or with `replace` in
/// place of the whole outline (`parent` must then be `[]`). A bookmark whose page this
/// document lacks is kept without a destination (see [`pdfcraft_organize::import_outline`]).
ImportBookmarks {
parent: Vec<usize>,
index: usize,
bookmarks: Vec<pdfcraft_organize::OutlineNode>,
replace: bool,
},
/// Bookmarks for the tagged headings (H, H1–H6), nested by level under a new first
/// "Untitled" bookmark (New Bookmarks from Structure).
BookmarksFromStructure,
/// Bookmarks for the lines set in `levels`' styles (the first is level 1, the next nests
/// under it, …), under a new first "Untitled" bookmark (#1693; see [`crate::text_style_bookmarks`]).
BookmarksFromTextStyles {
levels: Vec<crate::text_style_bookmarks::TextStyle>,
},
/// Label pages `from..=to` (0-based) as Acrobat's "Number pages" does; later pages keep their labels.
NumberPages {
from: usize,
to: usize,
style: pdfcraft_organize::LabelStyle,
prefix: String,
first: u32,
},
/// Add a calibrated distance, perimeter or area annotation.
AddMeasurement(measure::NewMeasurement),
/// Store a drawing scale for a rectangular viewport (PDF user space).
SetMeasurementScale {
page: usize,
bbox: [f64; 4],
name: String,
scale: measure::Scale,
},
/// Add a comment (sticky note, highlight, shape, drawing, text box…).
AddAnnotation(NewAnnotation),
/// A custom stamp from a picture file (a PDF page or an image) on `page`. A zero-size
/// `rect` is a point: the stamp is centred there at its natural size (at most 200 pt).
/// Any other `rect` is a box: the picture is scaled to fit inside it, keeping its shape,
/// and centred in it (#4077).
/// `line` is a text line under the picture, already filled in ([`Session::stamp_line`](crate::Session::stamp_line)).
AddCustomStamp {
page: usize,
rect: [f64; 4],
name: String,
file: MarkFile,
author: String,
line: Option<String>,
},
/// Rich Media ▸ Add 3D: a U3D or PRC `model` as a 3D annotation in `rect` on `page`, named
/// `name` (its alternate description). Its poster is the picture in `poster` (a PDF page or an
/// image) fitted inside `rect`, keeping its shape and centred, or a labelled frame without one.
AddModel3d {
page: usize,
rect: [f64; 4],
model: Arc<Vec<u8>>,
poster: Option<MarkFile>,
activation: pdfcraft_annot::Activation3d,
name: String,
},
/// Delete the comment at `index` in the page's `/Annots`, with its pop-up and replies.
DeleteAnnotation {
page: usize,
index: usize,
},
SetAnnotationContents {
page: usize,
index: usize,
text: String,
},
ReplyToAnnotation {
page: usize,
index: usize,
text: String,
author: String,
},
/// Acrobat's "Set status" (a state reply by `author`).
SetAnnotationStatus {
page: usize,
index: usize,
state: ReviewState,
author: String,
},
/// The Eraser: rub out the parts of drawing `(page, index)` near `path` (user space).
EraseInk {
page: usize,
index: usize,
path: Vec<[f64; 2]>,
radius: f64,
},
/// Replace Text: strike out `quads` and add a grouped caret holding `text`.
ReplaceText {
page: usize,
quads: Vec<[f64; 8]>,
text: String,
author: String,
strike: Style,
caret: Style,
},
/// Acrobat's "Mark with checkmark" (a hidden `/StateModel /Marked` reply by `author`).
MarkAnnotation {
page: usize,
index: usize,
marked: bool,
author: String,
},
/// Properties ▸ Locked.
LockAnnotation {
page: usize,
index: usize,
locked: bool,
},
MoveAnnotation {
page: usize,
index: usize,
dx: f64,
dy: f64,
},
/// Reshape a line, connected lines, a polygon or a cloud: its points in user space (a line's
/// two ends, the others' corners).
ReshapeAnnotation {
page: usize,
index: usize,
points: Vec<[f64; 2]>,
},
/// Open, close or move a comment's pop-up window (`rect` in user space; a comment without a
/// pop-up gets one).
SetAnnotationPopup {
page: usize,
index: usize,
rect: Option<[f64; 4]>,
open: Option<bool>,
},
/// Resize a rectangle, oval or text box.
ResizeAnnotation {
page: usize,
index: usize,
rect: [f64; 4],
},
StyleAnnotation {
page: usize,
index: usize,
color: Option<Rgb>,
opacity: Option<f64>,
width: Option<f64>,
/// Line or polyline: two endings. Callout: one. `None` leaves `/LE` unchanged.
endings: Option<Vec<pdfcraft_annot::LineEnding>>,
},
/// Fill a rectangle, oval or polygon comment, or remove its fill (`None`).
FillAnnotation {
page: usize,
index: usize,
fill: Option<Rgb>,
},
/// Draw a rectangle's, oval's, polygon's or text box's fill, or a text box's text, at an
/// opacity of its own (0–1), the line or border keeping the comment's; `None` leaves that
/// part as it is. `StyleAnnotation`'s opacity sets the whole comment's.
PartOpacity {
page: usize,
index: usize,
fill: Option<f64>,
text: Option<f64>,
},
/// Change the font size (pt) of a text box, typewriter text or callout.
FontSizeAnnotation {
page: usize,
index: usize,
size: f64,
},
/// Give a text box, typewriter text or callout a border colour of its own (`None`: the
/// text's again).
BorderColorAnnotation {
page: usize,
index: usize,
color: Option<Rgb>,
},
/// Give a text box or typewriter text an oval, a circle or a rectangle for its border: the
/// text box keeps its place, the oval or circle goes around it.
BorderShapeAnnotation {
page: usize,
index: usize,
shape: BorderShape,
},
/// Comment properties ▸ General / note icon.
SetAnnotationInfo {
page: usize,
index: usize,
author: Option<String>,
subject: Option<String>,
icon: Option<NoteIcon>,
},
/// Fill in a form field.
SetFieldValue {
name: String,
value: FieldValue,
},
/// Clear form: the named fields (all when `None`) back to their defaults.
ResetForm {
names: Option<Vec<String>>,
},
/// Prepare a form: add a field on a page (`rect` in PDF user space); `name` defaults to
/// Acrobat's next free name ("Text1", "Check Box2", …).
AddField {
page: usize,
rect: [f64; 4],
kind: NewField,
name: Option<String>,
},
/// Field Properties ▸ General / Options.
SetFieldProps {
name: String,
props: Box<FieldProps>,
},
/// An image field (or any button): show this image file (PNG, JPEG, TIFF, GIF, BMP).
SetFieldImage {
name: String,
image: Arc<Vec<u8>>,
},
/// Set the picture of one exact push-button field; legacy name-only calls remain available.
SetFieldImageTarget {
name: String,
object: pdfcraft_cos::ObjRef,
image: Arc<Vec<u8>>,
},
/// Duplicate a field onto other pages (same field, shared value).
DuplicateField {
name: String,
pages: Vec<usize>,
},
DeleteField {
name: String,
},
/// Delete one widget of a field (`widget`, 0-based in the field's widgets): one radio button
/// of a group, or one copy of a duplicated field. The last widget takes the field with it.
DeleteWidget {
name: String,
widget: usize,
},
/// Page properties ▸ Tab order, for pages (0-based).
SetTabOrder {
pages: Vec<usize>,
order: TabOrder,
},
/// Document Properties ▸ Initial View (and the language and binding).
SetInitialView(Box<InitialView>),
/// Print production ▸ Add printer marks: trim and bleed marks, registration targets, colour
/// bars and page information outside the trim box, as a page mark; the page may be widened
/// to make room (`settings.make_room`), so the page geometry is read again.
AddPrinterMarks {
pages: Vec<usize>,
settings: PrinterMarks,
replace: bool,
},
/// Print production ▸ Fix hairlines: strokes narrower than `settings.narrower_than` points on
/// the page (after every transformation; zero-width lines always) get `settings.width`.
FixHairlines {
pages: Vec<usize>,
settings: Hairlines,
},
/// Edit ▸ Attach a file: a document-level attachment (`/Names /EmbeddedFiles`) under
/// `name`, made unique; `mime` is the embedded file's `/Subtype` when known.
AddAttachment {
name: String,
bytes: Vec<u8>,
description: Option<String>,
mime: Option<String>,
},
/// Remove the document-level attachment whose name-tree key is `key`.
DeleteAttachment {
key: String,
},
/// Print production ▸ Convert colours: every colour on the pages in grayscale, RGB or CMYK.
ConvertColours {
pages: Vec<usize>,
settings: pdfcraft_edit::ConvertColours,
},
/// Change or clear only `/Desc` on a document-level attachment's file specification.
EditAttachmentDescription {
key: String,
description: Option<String>,
},
/// Build a tagged structure tree for an untagged, text-only document.
AutoTagText,
/// Add alternate text: set (or clear, with `None`) a figure's alternate text. `figure` is
/// the figure element's object number (from `Document::figures`).
SetAltText {
figure: u32,
alt: Option<String>,
},
/// Add `/Form` tags and object references for all widgets in an untagged AcroForm.
AutoTagFormFields,
/// Change one property on a reachable indirect structure tag.
SetTagProperty {
object: pdfcraft_cos::ObjRef,
property: a11y::TagProperty,
value: Option<String>,
},
/// Move a structure tag before or after a direct sibling in reading order.
MoveTag {
object: pdfcraft_cos::ObjRef,
anchor: pdfcraft_cos::ObjRef,
placement: a11y::TagPlacement,
},
/// Add alternate text ▸ Decorative figure: the figure's content becomes an artifact.
MarkDecorative {
figure: u32,
},
/// Tags panel ▸ Mark as artifact: retire a leaf tag and mark its content as an artifact.
MarkTagArtifact {
object: pdfcraft_cos::ObjRef,
},
/// A layout table as plain blocks: the table, its row groups and rows become Div tags and its
/// cells P tags (Div when they hold tags), without their Table attributes.
LayoutTableToBlocks {
object: pdfcraft_cos::ObjRef,
},
/// Document JavaScripts: add, replace (`script`) or remove (`None`) the document-level
/// script `name`.
SetDocumentScript {
name: String,
script: Option<String>,
},
/// Field Properties ▸ a custom script: set or remove field `name`'s JavaScript for `event`
/// (keystroke, format, validate, calculate or mouse_up).
SetFieldScript {
name: String,
event: String,
script: Option<String>,
},
/// Standards ▸ Save as PDF/A: fix what can be fixed for `level` (metadata, output intent,
/// forbidden actions, annotation flags, …).
ConvertPdfA {
level: pdfcraft_preflight::Level,
},
/// Remove what identifies the document as meeting `standard` (PDF/A or PDF/X).
RemoveStandardId {
standard: Standard,
},
/// What a script (button or console) changed in form fields: values, appearance colours,
/// read-only, required and visibility.
ApplyScriptChanges {
changes: Vec<pdfcraft_forms::FieldChange>,
},
/// An XFA form's scripted event (a button's `click`, say) on the object at `som`: what the
/// script changes (values, rows, visibility) is applied and the form laid out again.
XfaEvent {
som: String,
activity: String,
},
/// Scan & OCR ▸ Recognize text: put recognised words on `page` as invisible text over the
/// image (from [`ocr::OcrJob::run`](crate::ocr::OcrJob::run)).
AddOcrText {
page: usize,
words: Vec<pdfcraft_ocr::PlacedWord>,
},
/// Scan & OCR ▸ Enhance ▸ Scanned document: put the scan images
/// [`ScanCleanJob::run`](crate::scan_clean::ScanCleanJob::run) cleaned in place of the old
/// ones (each object must still be an image). Built by
/// [`Session::apply_scan_clean`](crate::Session::apply_scan_clean).
EnhanceScan {
images: Vec<(pdfcraft_cos::ObjRef, pdfcraft_cos::Stream)>,
},
/// Edit a PDF ▸ Edit text: replace the text of line `line` (from `Document::text_lines`) on
/// `page`, in its own font when it can show it, else in Helvetica.
EditTextLine {
page: usize,
line: usize,
text: String,
},
/// Edit a PDF ▸ an existing image on `page` (index from `Document::page_images`).
EditPageImage {
page: usize,
index: usize,
change: ImageEdit,
},
/// Move mixed existing and added objects together. References resolve before any change;
/// offset is a user-space vector. No retyping, reflow or font substitution is involved.
MoveObjects {
page: usize,
objects: Vec<ObjectTarget>,
offset: [f64; 2],
},
/// Delete mixed existing and added objects together (references from one inventory of
/// `page`), as one step: what Delete and Cut do to objects selected together.
DeleteObjects {
page: usize,
objects: Vec<ObjectTarget>,
},
/// Edit text in a paragraph box: replace paragraph `block` (from `Document::text_blocks`),
/// rewrapped to the box's width.
EditTextBlock {
page: usize,
block: usize,
text: String,
/// Formatting changes (font, size, colour, alignment); default keeps the paragraph's.
style: pdfcraft_edit::BlockStyle,
},
/// Expose supported grouped source text on one page for direct editing.
PrepareText {
page: usize,
},
/// An explicit open-licensed full font program, completing subsets or selected by a user.
RegisterTextFont {
page: usize,
bytes: Arc<Vec<u8>>,
},
/// Order tabs manually: move a field one place earlier or later on its page.
MoveInTabOrder {
name: String,
earlier: bool,
},
/// Set the calculation order: these calculated fields first, in this order, then the others
/// in their current order (see [`Document::calculation_order`](crate::Document::calculation_order)). The form recalculates.
SetCalculationOrder {
names: Vec<String>,
},
/// Add a header and footer (with `replace`, existing ones on those pages go first).
AddHeaderFooter {
pages: Vec<usize>,
settings: HeaderFooter,
replace: bool,
},
/// `file`: a picture from a file (an image, or a page of a PDF) instead of text.
AddWatermark {
pages: Vec<usize>,
settings: Watermark,
replace: bool,
file: Option<MarkFile>,
},
/// `file`: a picture from a file instead of a colour.
AddBackground {
pages: Vec<usize>,
settings: Background,
replace: bool,
file: Option<MarkFile>,
},
/// Remove every mark of a kind from every page.
RemoveMarks {
kind: MarkKind,
},
/// Edit a PDF ▸ Add content ▸ Text (`text.rect` in display space).
AddText {
page: usize,
text: AddedText,
},
/// Edit a PDF ▸ Add content ▸ Image: an image file placed at `rect` (display space), or at
/// its natural size centred on the page (shrunk to fit).
AddImage {
page: usize,
rect: Option<[f64; 4]>,
name: String,
bytes: Arc<Vec<u8>>,
},
/// Move, resize, retype or reformat an added item (`index` among the page's added items).
UpdateContent {
page: usize,
index: usize,
content: AddedContent,
},
DeleteContent {
page: usize,
index: usize,
},
/// Replace an added image with another file (keeping its box and transforms).
ReplaceImage {
page: usize,
index: usize,
name: String,
bytes: Arc<Vec<u8>>,
},
/// Edit ▸ Paste: an added item copied in this document or another, as it was: text with its
/// style, an image in its box (display space) with its turns, flips and crop. An image's
/// picture is `file` (from [`Document::added_image_file`](crate::Document::added_image_file)): the copied image object is reused
/// while it is still that picture in this document, else one an added image here already draws
/// it from, else the file is embedded.
PasteContent {
page: usize,
content: AddedContent,
file: Option<(String, Arc<Vec<u8>>)>,
},
/// Edit ▸ Paste of objects copied together in Edit a PDF ([`Document::copy_objects`](crate::Document::copy_objects)), in this
/// document or another: drawn on top of `page` as they were, moved by `offset` (user space).
PasteObjects {
page: usize,
clip: Arc<crate::ObjectClip>,
offset: [f64; 2],
},
/// Apply redaction marks (all, or those on `pages`): remove what they cover for good.
ApplyRedactions {
pages: Option<Vec<usize>>,
},
/// Remove redaction marks without applying them.
ClearRedactions,
/// Edit a PDF ▸ Link: a new link over `rect` (user space).
AddLink {
page: usize,
rect: [f64; 4],
action: LinkAction,
style: LinkStyle,
},
/// Link Properties (`index` in the page's `/Annots`).
SetLink {
page: usize,
index: usize,
rect: Option<[f64; 4]>,
action: Option<LinkAction>,
style: Option<LinkStyle>,
},
DeleteLink {
page: usize,
index: usize,
},
/// Remove all links (on `pages`, or everywhere).
RemoveLinks {
pages: Option<Vec<usize>>,
},
/// Create links from URLs in the text: (page, line rects in user space, URI).
AddLinks {
links: Vec<(usize, Vec<[f64; 4]>, String)>,
style: LinkStyle,
},
/// Import comments and/or form data (XFDF, FDF, XML, CSV, tab-delimited text), or the
/// comments of another PDF onto the pages with the same numbers.
ImportData {
name: String,
bytes: Arc<Vec<u8>>,
},
/// Paste copied comments (XFDF from [`Session::export_comments`]) onto `page` as new
/// comments: copies under names of their own, so nothing is replaced and pasting twice
/// gives two sets; replies keep their copied parent.
PasteComments {
xfdf: String,
page: usize,
/// Where the copies go: moved by (dx, dy) in user space, or at their own positions.
offset: Option<[f64; 2]>,
},
/// Remove Hidden Information: the chosen categories.
RemoveHidden {
which: Vec<Hidden>,
},
/// Sanitize Document: every category, then a full rewrite on save.
Sanitize,
/// An XFA form made an ordinary (static) PDF form: the XFA packets, `/NeedsRendering` and
/// PdfCraft's private keys go, the pages as laid out and the fields with their values
/// stay, so every viewer shows the same form and none lays it out again. Its scripts no
/// longer run.
XfaToStatic,
/// Flatten comments and/or form fields on every page into page content.
Flatten {
comments: bool,
fields: bool,
},
/// Bake Fill & Sign text, marks and signatures into the page. Other comments and fields stay.
/// A no-op (no undo step) when the document has none.
FlattenFillSign,
/// Protect with passwords and permissions (written by the next save, which is a full rewrite).
Protect(Protection),
/// Remove password security (needs the owner password).
RemoveProtection,
/// Several edits applied as one undoable step (all or nothing).
Batch {
label: String,
edits: Vec<Edit>,
},
}