diff --git a/ROADMAP.md b/ROADMAP.md
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -745,3 +745,4 @@ Newest first: the date, what moved, and the new overall estimate.
 - **2026-10-18 (renderer, hayro patches 19/35):** A mesh shading (types 4–7) is sampled over its own triangles instead of the whole path: a BricsCAD plan with 310 one-triangle `sh` fills took 2 s per 1024 px tile at every zoom (and hit the 20 s watchdog on a busy machine); now about 70 ms, so zooming into dense topology drawings stays fluid. Estimates unchanged.
 
 - **2026-10-22 (PR #3227 integration):** M11: integrate upstream `80b7d215`; retain partial target-version compatibility and upstream content-boundary and image-size regression coverage. Scope and estimates unchanged.
+- **2026-10-27 (M7, #4957):** Remove watermark also removes watermarks another editor wrote as artifacts (`/Artifact <</Subtype /Watermark>> BDC … EMC`, inline or through `/Properties`): the marked content is cut out byte for byte, anything unbalanced or unreadable is left. `doc_remove_marks` and the Edit menu command both go through it. A watermark whose drawing changes state that later content inherits, or a page that can't be read, stops the removal with the page and the reason, and the command is offered for a document that has only such a watermark. Update and reading a foreign watermark's settings into the dialog are not done.
diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs
--- a/crates/automation/src/tools.rs
+++ b/crates/automation/src/tools.rs
@@ -1002,7 +1002,7 @@ pub fn tools() -> Vec<ToolDef> {
                 json!({ "doc": doc(), "pages": pages("to fill (default: all)"), "color": { "type": "string" }, "file": { "type": "string" }, "file_page": { "type": "integer", "minimum": 1 }, "scale": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 }, "opacity": { "type": "number", "minimum": 0, "maximum": 1 }, "replace": { "type": "boolean" } }),
                 &["doc"],
             )),
-        t("doc_remove_marks", "Remove header & footer, watermark or background", "Remove every header and footer, watermark or background PdfCraft (or a compatible tool) added. Undoable.")
+        t("doc_remove_marks", "Remove header & footer, watermark or background", "Remove every header and footer, watermark or background PdfCraft (or a compatible tool) added. Removing a watermark also removes the ones another editor marked as watermark artifacts (Acrobat and others write them as /Artifact /Subtype /Watermark in the page content). Undoable.")
             .destructive()
             .with(schema(json!({ "doc": doc(), "kind": { "type": "string", "enum": ["header_footer", "watermark", "background"] } }), &["doc", "kind"])),
         t("doc_export_images", "Export pages as images", "Write pages as PNG, JPEG or TIFF files (`<name>_page_<n>.png|jpg|tif`) into a folder, at a resolution (default 150 dpi). JPEG and TIFF are flattened onto white paper. A page too large for the renderer at that resolution is drawn at the most it allows, listed in `lower_dpi` ({page, dpi}). Includes unsaved edits.")
diff --git a/crates/automation/tests/it/automation.rs b/crates/automation/tests/it/automation.rs
--- a/crates/automation/tests/it/automation.rs
+++ b/crates/automation/tests/it/automation.rs
@@ -1122,6 +1122,83 @@ fn combine_takes_images_and_text_files() {
     }
 }
 
+/// A PDF with one page per entry of `contents`, the standard font as /F1 on each.
+fn pages_with_content(contents: &[&str]) -> Vec<u8> {
+    let n = contents.len();
+    let kids: Vec<String> = (0..n).map(|i| format!("{} 0 R", 3 + i)).collect();
+    let mut objs = vec![
+        "<< /Type /Catalog /Pages 2 0 R >>".to_string(),
+        format!("<< /Type /Pages /Kids [{}] /Count {n} /MediaBox [0 0 300 300] >>", kids.join(" ")),
+    ];
+    for i in 0..n {
+        objs.push(format!("<< /Type /Page /Parent 2 0 R /Contents {} 0 R /Resources << /Font << /F1 {} 0 R >> >> >>", 3 + n + i, 3 + 2 * n));
+    }
+    for content in contents {
+        objs.push(format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()));
+    }
+    objs.push("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".to_string());
+    let mut out = b"%PDF-1.7\n".to_vec();
+    let mut offsets = Vec::new();
+    for (i, o) in objs.iter().enumerate() {
+        offsets.push(out.len());
+        out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes());
+    }
+    let xref = out.len();
+    out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
+    for o in offsets {
+        out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
+    }
+    out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
+    out
+}
+
+/// A one-page PDF whose content is `content`, with the standard font as /F1.
+fn page_with_content(content: &str) -> Vec<u8> {
+    pages_with_content(&[content])
+}
+
+/// #4957: Remove watermark takes out a watermark another editor marked as an artifact, leaves the
+/// page's own text, and says so when there is none.
+#[test]
+fn remove_marks_takes_out_watermarks_other_editors_marked_as_artifacts() {
+    let dir = workdir("foreign-watermark");
+    let content = "BT /F1 18 Tf 20 250 Td (Body text) Tj ET\n/Artifact <</Subtype /Watermark /Type /Pagination>> BDC q BT /F1 40 Tf 20 100 Td (CONFIDENTIAL) Tj ET Q EMC\nBT /F1 12 Tf 20 20 Td (Footer) Tj ET";
+    std::fs::write(dir.join("marked.pdf"), page_with_content(content)).unwrap();
+    let mut a = auto(&dir);
+    let doc = ok(&mut a, "doc_open", json!({ "path": "marked.pdf" }))["doc"].as_u64().unwrap();
+    assert!(page_text(&mut a, doc)[0].contains("CONFIDENTIAL"));
+    ok(&mut a, "doc_remove_marks", json!({ "doc": doc, "kind": "watermark" }));
+    let text = page_text(&mut a, doc)[0].clone();
+    assert!(!text.contains("CONFIDENTIAL") && text.contains("Body text") && text.contains("Footer"), "{text}");
+    // Undo brings it back; a second removal after redo has nothing to take out.
+    ok(&mut a, "edit_undo", json!({ "doc": doc }));
+    assert!(page_text(&mut a, doc)[0].contains("CONFIDENTIAL"), "undo restores the watermark");
+    ok(&mut a, "edit_redo", json!({ "doc": doc }));
+    assert!(
+        matches!(a.call("doc_remove_marks", &json!({ "doc": doc, "kind": "watermark" })), Err(ToolError::Failed(e)) if e.contains("nothing to remove"))
+    );
+    // Other kinds are not touched by it: an artifact that is not a watermark stays.
+    let header = "/Artifact <</Subtype /Header /Type /Pagination>> BDC BT /F1 10 Tf 20 280 Td (Running header) Tj ET EMC";
+    std::fs::write(dir.join("header.pdf"), page_with_content(header)).unwrap();
+    let doc = ok(&mut a, "doc_open", json!({ "path": "header.pdf" }))["doc"].as_u64().unwrap();
+    assert!(matches!(a.call("doc_remove_marks", &json!({ "doc": doc, "kind": "watermark" })), Err(ToolError::Failed(_))));
+    assert!(page_text(&mut a, doc)[0].contains("Running header"));
+
+    // A watermark that can't be cut out cleanly on one page stops the whole removal, naming the
+    // page and why, and changes nothing: the page that could have been cleaned is left as it was.
+    let clean = "BT /F1 12 Tf 20 20 Td (Page one body) Tj ET /Artifact <</Subtype /Watermark>> BDC q BT /F1 40 Tf 20 100 Td (STAMP) Tj ET Q EMC";
+    let leaking = "0 g /Artifact <</Subtype /Watermark>> BDC 1 0 0 rg BT /F1 40 Tf 20 100 Td (LEAKY) Tj ET EMC BT /F1 12 Tf (Body two) Tj ET";
+    std::fs::write(dir.join("mixed.pdf"), pages_with_content(&[clean, leaking])).unwrap();
+    let doc = ok(&mut a, "doc_open", json!({ "path": "mixed.pdf" }))["doc"].as_u64().unwrap();
+    let refused = a.call("doc_remove_marks", &json!({ "doc": doc, "kind": "watermark" }));
+    assert!(
+        matches!(&refused, Err(ToolError::Failed(e)) if e.contains("page 2") && e.contains("nothing was changed") && !e.contains("page 1")),
+        "{refused:?}"
+    );
+    let text = page_text(&mut a, doc);
+    assert!(text[0].contains("STAMP") && text[1].contains("LEAKY"), "nothing changed: {text:?}");
+}
+
 #[test]
 fn combine_extract_and_split() {
     let dir = workdir("organize");
diff --git a/crates/edit/src/foreign_marks.rs b/crates/edit/src/foreign_marks.rs
new file mode 100644
--- /dev/null
+++ b/crates/edit/src/foreign_marks.rs
@@ -0,0 +1,321 @@
+//! Watermarks other editors wrote (#4957): a page's content marked as an artifact of subtype
+//! Watermark, `/Artifact <</Subtype /Watermark /Type /Pagination>> BDC … EMC` (ISO 32000-2
+//! §14.8.2.2), which is how Acrobat and the other PDF editors mark theirs. The properties may be
+//! inline or a name in the page's `/Properties`. Removing one cuts exactly those bytes out of the
+//! content; everything else is copied as it was.
+//!
+//! Marked content does not save and restore the graphics state, so a watermark is cut out only
+//! when nothing it does can reach what is drawn after it: every state change inside it sits
+//! inside its own `q … Q`, and it opens and closes no text object or path the content around it
+//! owns. One that does anything else is reported, not removed.
+
+use std::fmt;
+use std::ops::Range;
+
+use super::*;
+use pdfcraft_content::{Op, Pieces};
+
+/// Marked-content scopes open at once beyond which a page is not read.
+const MAX_SCOPES: usize = 1024;
+/// The most decoded content bytes read from one document in one go, and operators in one page.
+const MAX_CONTENT_BYTES: usize = 256 * 1024 * 1024;
+const MAX_OPERATORS: usize = 5_000_000;
+/// Pages looked at to tell whether a document has a watermark of this kind at all.
+const MAX_DETECT_PAGES: usize = 5_000;
+/// The deepest `q` nesting read (a page nested deeper than any real one is not read).
+const MAX_DEPTH: usize = 100_000;
+
+/// Why a page's watermark was not removed, or the page not checked.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Blocked {
+    /// A content stream could not be read (damaged, too large).
+    Unreadable,
+    /// The content could not be read with certainty: bytes the parser skipped, an inline image
+    /// whose end is a guess, marked-content scopes that do not pair up.
+    Uncertain,
+    /// A watermark is there, but cutting it out would change what is drawn after it.
+    Unsafe,
+}
+
+impl fmt::Display for Blocked {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_str(match self {
+            Blocked::Unreadable => "its content can't be read",
+            Blocked::Uncertain => "its content can't be read with certainty",
+            Blocked::Unsafe => "a watermark there changes what is drawn after it, so it can't be removed cleanly",
+        })
+    }
+}
+
+/// What [`remove_foreign_watermarks`] did.
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct ForeignWatermarks {
+    /// Watermark artifacts removed.
+    pub removed: usize,
+    /// Pages (0-based) a watermark could not be removed from, or that could not be checked.
+    pub blocked: Vec<(usize, Blocked)>,
+}
+
+/// A marked-content scope that is open.
+struct Scope {
+    watermark: bool,
+    /// The graphics-state depth (`q` minus `Q`) where it opened, and the lowest it reached since.
+    depth: usize,
+    floor: usize,
+    start: usize,
+    /// Which operator opened it (counted from 1), to tell whether a state change came after.
+    opened: usize,
+    /// A text object or a path was open where it started.
+    text: bool,
+    path: bool,
+}
+
+/// Operators that change the graphics state, the colour state or the text state, or the clip: they
+/// outlast the marked-content scope they are in, until the `Q` of the `q` around them.
+fn changes_state(op: &[u8]) -> bool {
+    matches!(
+        op,
+        b"cm"
+            | b"w"
+            | b"J"
+            | b"j"
+            | b"M"
+            | b"d"
+            | b"ri"
+            | b"i"
+            | b"gs"
+            | b"CS"
+            | b"cs"
+            | b"SC"
+            | b"SCN"
+            | b"sc"
+            | b"scn"
+            | b"G"
+            | b"g"
+            | b"RG"
+            | b"rg"
+            | b"K"
+            | b"k"
+            | b"Tc"
+            | b"Tw"
+            | b"Tz"
+            | b"TL"
+            // `"` sets the word and character spacing and then shows text; `TD` sets the leading.
+            | b"\""
+            | b"TD"
+            | b"Tf"
+            | b"Tr"
+            | b"Ts"
+            | b"W"
+            | b"W*"
+    )
+}
+
+/// The value of `/Subtype` in `d`, following a reference, is the name Watermark.
+fn subtype_is_watermark(doc: &Document, d: &Dict) -> bool {
+    d.get(b"Subtype").map(|o| doc.resolve(o)).is_some_and(|v| v.as_name() == Some(b"Watermark"))
+}
+
+/// The page's `/Properties` resource dictionary, if it has one.
+fn page_properties(doc: &Document, page: &pdfcraft_model::Page) -> Option<Dict> {
+    page.dict
+        .get(b"Resources")
+        .map(|r| doc.resolve(r))
+        .and_then(|r| r.as_dict().and_then(|d| d.get(b"Properties").cloned()))
+        .map(|p| doc.resolve(&p))
+        .and_then(|p| p.as_dict().cloned())
+}
+
+/// Whether a `BDC` operator opens a watermark artifact.
+fn opens_watermark(doc: &Document, properties: Option<&Dict>, op: &Op) -> bool {
+    if op.name(0) != Some(b"Artifact") {
+        return false;
+    }
+    match op.operands.get(1) {
+        Some(Object::Dict(d)) => subtype_is_watermark(doc, d),
+        Some(Object::Name(name)) => properties
+            .and_then(|p| p.get(name))
+            .map(|o| doc.resolve(o))
+            .is_some_and(|resolved| resolved.as_dict().is_some_and(|d| subtype_is_watermark(doc, d))),
+        _ => false,
+    }
+}
+
+/// What a page's content holds.
+enum Scan {
+    /// No watermark artifact.
+    Clean,
+    /// Watermark artifacts: the byte ranges (in the joined content) that can be cut out, outermost
+    /// ones only, and how many more cannot be.
+    Found { spans: Vec<Range<usize>>, unsafe_left: usize },
+    /// Not read with certainty.
+    Uncertain,
+}
+
+fn scan_page(doc: &Document, page: &pdfcraft_model::Page, pieces: &Pieces) -> Scan {
+    let properties = page_properties(doc, page);
+    // Most pages have none: a watermark names itself in the content, or through a property whose
+    // subtype says so. Neither: no need to read the content operator by operator. A name may be
+    // spelled with `#` escapes (`/Water#6Dark` is `/Watermark`), so content with a `#` in it is
+    // read in full: the absence of the plain spelling proves nothing there.
+    let named = properties.as_ref().is_some_and(|p| p.iter().any(|(_, v)| doc.resolve(v).as_dict().is_some_and(|d| subtype_is_watermark(doc, d))));
+    let data = pieces.data();
+    if !named && !data.contains(&b'#') && !data.windows(9).any(|w| w == b"Watermark") {
+        return Scan::Clean;
+    }
+    let parsed = pieces.parse();
+    if parsed.skipped != 0 || parsed.ambiguous_images != 0 || parsed.ops.len() > MAX_OPERATORS {
+        return Scan::Uncertain;
+    }
+    let (mut depth, mut stack, mut spans, mut unsafe_left) = (0usize, Vec::<Scope>::new(), Vec::<Range<usize>>::new(), 0usize);
+    let (mut text, mut path) = (false, false);
+    // For each `q` depth, the count of the operator that last changed state there. A scope is
+    // affected when it was opened before the last change at its own depth: one lookup, however
+    // many scopes are open.
+    let mut last_change: Vec<usize> = vec![0];
+    for (index, op) in parsed.ops.iter().enumerate() {
+        let count = index.saturating_add(1);
+        let name = op.op.as_slice();
+        match name {
+            b"q" => {
+                depth = depth.saturating_add(1);
+                if depth > MAX_DEPTH {
+                    return Scan::Uncertain;
+                }
+                if depth >= last_change.len() {
+                    last_change.resize(depth + 1, 0);
+                }
+            }
+            b"Q" => {
+                depth = depth.saturating_sub(1);
+                if let Some(top) = stack.last_mut() {
+                    top.floor = top.floor.min(depth);
+                }
+            }
+            b"BT" => text = true,
+            b"ET" => text = false,
+            b"m" | b"l" | b"c" | b"v" | b"y" | b"h" | b"re" => path = true,
+            b"S" | b"s" | b"f" | b"F" | b"f*" | b"B" | b"B*" | b"b" | b"b*" | b"n" => path = false,
+            b"BMC" | b"BDC" => {
+                if stack.len() >= MAX_SCOPES {
+                    return Scan::Uncertain;
+                }
+                let watermark = name == b"BDC" && opens_watermark(doc, properties.as_ref(), op);
+                stack.push(Scope { watermark, depth, floor: depth, start: op.span.start, opened: count, text, path });
+            }
+            b"EMC" => {
+                let Some(scope) = stack.pop() else { return Scan::Uncertain };
+                if let Some(parent) = stack.last_mut() {
+                    parent.floor = parent.floor.min(scope.floor);
+                }
+                if scope.watermark && !stack.iter().any(|s| s.watermark) {
+                    // Where a text object or a path was open around it, it would change that
+                    // object's matrix or that path (`q … Q` restores neither), so such a scope is
+                    // never cut: nor one that closes or opens one, or changes state after it.
+                    let effects = last_change.get(scope.depth).is_some_and(|last| *last > scope.opened);
+                    let clean = depth == scope.depth && scope.floor >= scope.depth && !effects && !scope.text && !scope.path && !text && !path;
+                    if clean {
+                        spans.push(scope.start..op.span.end);
+                    } else {
+                        unsafe_left += 1;
+                    }
+                }
+            }
+            _ => {}
+        }
+        // A state change at a `q` depth reaches every scope open at that depth.
+        if changes_state(name)
+            && let Some(slot) = last_change.get_mut(depth)
+        {
+            *slot = count;
+        }
+    }
+    if !stack.is_empty() {
+        return Scan::Uncertain;
+    }
+    if spans.is_empty() && unsafe_left == 0 { Scan::Clean } else { Scan::Found { spans, unsafe_left } }
+}
+
+/// A page's content streams decoded, within what is left of `budget`.
+fn page_streams(doc: &Document, page: &pdfcraft_model::Page, budget: &mut usize) -> Option<Vec<(Object, Vec<u8>)>> {
+    let list: Vec<Object> = match page.dict.get(b"Contents") {
+        None => Vec::new(),
+        Some(c) => match &*doc.resolve(c) {
+            Object::Array(a) => a.clone(),
+            _ => vec![c.clone()],
+        },
+    };
+    let mut out = Vec::with_capacity(list.len().min(1024));
+    for object in list {
+        let resolved = doc.resolve(&object);
+        let Object::Stream(stream) = &*resolved else { return None };
+        let bytes = stream.decoded_within(*budget).ok()?;
+        *budget = budget.checked_sub(bytes.len())?;
+        out.push((object, bytes));
+    }
+    Some(out)
+}
+
+/// Whether the document may have a watermark artifact another editor wrote (for enabling Remove
+/// watermark). `None` when the document is too large to look through, or a page can't be read:
+/// then it may.
+pub fn foreign_watermarks_present(doc: &Document) -> Option<bool> {
+    let pages = page_list(doc);
+    if pages.len() > MAX_DETECT_PAGES {
+        return None;
+    }
+    let mut budget = MAX_CONTENT_BYTES;
+    let mut unknown = false;
+    for page in &pages {
+        let Some(streams) = page_streams(doc, page, &mut budget) else {
+            unknown = true;
+            continue;
+        };
+        let pieces = Pieces::join(&streams.iter().map(|(_, bytes)| bytes.as_slice()).collect::<Vec<_>>());
+        match scan_page(doc, page, &pieces) {
+            Scan::Found { .. } => return Some(true),
+            Scan::Uncertain => unknown = true,
+            Scan::Clean => {}
+        }
+    }
+    if unknown { None } else { Some(false) }
+}
+
+/// Remove the watermark artifacts other editors wrote from `pages` (0-based). A page whose
+/// watermark can't be cut out cleanly, or whose content can't be read with certainty, is left as
+/// it is and listed in the result, so the caller can say so: nothing is skipped silently. The
+/// fonts, forms and images a removed watermark drew with stay in the page's resources, unused.
+pub fn remove_foreign_watermarks(doc: &mut Document, pages: &[usize]) -> Result<ForeignWatermarks, EditError> {
+    let all = page_list(doc);
+    check(pages, all.len())?;
+    let mut result = ForeignWatermarks::default();
+    let mut budget = MAX_CONTENT_BYTES;
+    for &index in pages {
+        let Some(page) = all.get(index) else { continue };
+        let Some(streams) = page_streams(doc, page, &mut budget) else {
+            result.blocked.push((index, Blocked::Unreadable));
+            continue;
+        };
+        if streams.is_empty() {
+            continue;
+        }
+        let pieces = Pieces::join(&streams.iter().map(|(_, bytes)| bytes.as_slice()).collect::<Vec<_>>());
+        match scan_page(doc, page, &pieces) {
+            Scan::Clean => {}
+            Scan::Uncertain => result.blocked.push((index, Blocked::Uncertain)),
+            Scan::Found { spans, unsafe_left } => {
+                if unsafe_left > 0 {
+                    result.blocked.push((index, Blocked::Unsafe));
+                }
+                if spans.is_empty() {
+                    continue;
+                }
+                let data = pieces.splice(spans.iter().map(|r| (r.clone(), Vec::new())));
+                let contents = crate::text::rewritten_contents(doc, &streams, data);
+                doc.update_dict(page.obj, |d| d.set(b"Contents".to_vec(), contents))?;
+                result.removed += spans.len();
+            }
+        }
+    }
+    Ok(result)
+}
diff --git a/crates/edit/src/lib.rs b/crates/edit/src/lib.rs
--- a/crates/edit/src/lib.rs
+++ b/crates/edit/src/lib.rs
@@ -57,10 +57,12 @@ pub use added::{
 };
 
 mod apply;
+mod foreign_marks;
 mod marks;
 mod place;
 
 pub use apply::{add_background, add_header_footer, add_watermark, marks_present, remove_marks};
+pub use foreign_marks::{Blocked, ForeignWatermarks, foreign_watermarks_present, remove_foreign_watermarks};
 #[cfg(test)]
 use marks::bates_start;
 pub use marks::{Background, Context, HeaderFooter, MarkKind, MarkSource, Watermark, expand};
diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs
--- a/crates/edit/src/tests.rs
+++ b/crates/edit/src/tests.rs
@@ -7457,3 +7457,280 @@ fn a_run_of_spaces_in_a_subset_without_a_space_glyph_keeps_its_width() {
     let content = String::from_utf8_lossy(&page_content_bytes(&reopen(&doc), 0)).into_owned();
     assert_eq!(content.matches("[-330] TJ").count(), 5, "{content}");
 }
+
+/// A one-page document whose page content is `streams` (each its own stream, in order), with
+/// `/Properties` `props` in its resources.
+fn page_of_streams(streams: &[&str], props: &str) -> Document {
+    page_of_streams_with(streams, props, &[])
+}
+
+/// The same, with `extra` objects numbered after the streams (the first is object `4 + streams`).
+fn page_of_streams_with(streams: &[&str], props: &str, extra: &[&str]) -> Document {
+    let mut objs: Vec<String> =
+        vec!["<< /Type /Catalog /Pages 2 0 R >>".into(), "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 200] >>".into()];
+    let refs: Vec<String> = (0..streams.len()).map(|i| format!("{} 0 R", 4 + i)).collect();
+    objs.push(format!("<< /Type /Page /Parent 2 0 R /Contents [{}] /Resources << /Properties << {props} >> >> >>", refs.join(" ")));
+    for body in streams {
+        objs.push(format!("<< /Length {} >>\nstream\n{body}\nendstream", body.len()));
+    }
+    objs.extend(extra.iter().map(|o| o.to_string()));
+    let mut out = b"%PDF-1.7\n".to_vec();
+    let mut offs = Vec::new();
+    for (i, o) in objs.iter().enumerate() {
+        offs.push(out.len());
+        out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes());
+    }
+    let x = out.len();
+    out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
+    for o in offs {
+        out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
+    }
+    out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{x}\n%%EOF\n", objs.len() + 1).as_bytes());
+    Document::open(Arc::new(out)).unwrap()
+}
+
+/// #4957: a watermark another editor marked as an artifact (inline or through `/Properties`) is
+/// cut out of the content byte for byte, and the rest of the page is left exactly as it was.
+#[test]
+fn foreign_watermark_artifacts_are_cut_out_and_nothing_else_changes() {
+    let wm = "/Artifact <</Subtype /Watermark /Type /Pagination>> BDC q 0.5 g /Fm0 Do Q EMC";
+    let content = format!("BT (Before) Tj ET\n{wm}\nBT (After) Tj ET");
+    let mut doc = page_of_streams(&[&content], "");
+    assert_eq!(remove_foreign_watermarks(&mut doc, &[0]).unwrap().removed, 1);
+    assert_eq!(streams(&doc, 0), ["BT (Before) Tj ET\n\nBT (After) Tj ET"], "only the artifact's bytes are gone");
+    let doc = reopen(&doc);
+    assert_eq!(streams(&doc, 0), ["BT (Before) Tj ET\n\nBT (After) Tj ET"], "and it is saved that way");
+
+    // Named properties, as Acrobat writes them.
+    let named = "BT (A) Tj ET /Artifact /MC0 BDC q /Fm0 Do Q EMC BT (B) Tj ET";
+    let mut doc = page_of_streams(&[named], "/MC0 << /Subtype /Watermark /Type /Pagination >>");
+    assert_eq!(remove_foreign_watermarks(&mut doc, &[0]).unwrap().removed, 1);
+    assert_eq!(streams(&doc, 0), ["BT (A) Tj ET  BT (B) Tj ET"]);
+
+    // Several, and one with marked content of its own inside.
+    let many = format!("{wm} BT (x) Tj ET /Artifact <</Subtype /Watermark>> BDC /Span BMC q Q EMC EMC");
+    let mut doc = page_of_streams(&[&many], "");
+    assert_eq!(remove_foreign_watermarks(&mut doc, &[0]).unwrap().removed, 2);
+    assert_eq!(streams(&doc, 0), [" BT (x) Tj ET "]);
+}
+
+/// #4957: only what is marked as a watermark goes, and only when cutting it out can't change what
+/// follows it.
+#[test]
+fn foreign_watermark_removal_leaves_everything_it_should() {
+    // Other artifacts and other marked content stay; a watermark inside other marked content goes
+    // alone, and the content around it stays.
+    let kept = "/Artifact <</Type /Pagination /Subtype /Header>> BDC (h) Tj EMC /Artifact BMC (b) Tj EMC /OC /MC0 BDC (o) Tj EMC /Artifact /MC1 BDC (n) Tj EMC";
+    let mut doc = page_of_streams(&[kept], "/MC0 << /Subtype /Watermark >> /MC1 << /Subtype /Footer >>");
+    assert_eq!(remove_foreign_watermarks(&mut doc, &[0]).unwrap(), ForeignWatermarks::default());
+    assert_eq!(streams(&doc, 0), [kept], "no watermark: the content is untouched");
+    let inside = "/Span BMC /Artifact <</Subtype /Watermark>> BDC (w) Tj EMC (keep) Tj EMC";
+    let mut doc = page_of_streams(&[inside], "");
+    assert_eq!(remove_foreign_watermarks(&mut doc, &[0]).unwrap().removed, 1);
+    assert_eq!(streams(&doc, 0), ["/Span BMC  (keep) Tj EMC"]);
+
+    // A watermark whose q and Q don't balance inside it stays: cutting it would change the state.
+    for unbalanced in ["/Artifact <</Subtype /Watermark>> BDC q 1 0 0 1 5 5 cm EMC (x) Tj Q", "q /Artifact <</Subtype /Watermark>> BDC Q q EMC Q"] {
+        let mut doc = page_of_streams(&[unbalanced], "");
+        assert_eq!(remove_foreign_watermarks(&mut doc, &[0]).unwrap().removed, 0, "{unbalanced}");
+        assert_eq!(streams(&doc, 0), [unbalanced]);
+    }
+
+    // Content that can't be read with certainty is left alone: an unclosed scope, a stray EMC.
+    for bad in [
+        "/Artifact <</Subtype /Watermark>> BDC q Q (x) Tj",
+        "(x) Tj EMC /Artifact <</Subtype /Watermark>> BDC EMC",
+        // A whole watermark, then a scope that is never closed: not read with certainty, so left.
+        "/Artifact <</Subtype /Watermark>> BDC q Q EMC /Span BMC (x) Tj",
+    ] {
+        let mut doc = page_of_streams(&[bad], "");
+        assert_eq!(remove_foreign_watermarks(&mut doc, &[0]).unwrap().removed, 0, "{bad}");
+        assert_eq!(streams(&doc, 0), [bad]);
+    }
+    // A page that isn't there is refused.
+    let mut doc = page_of_streams(&["(x) Tj"], "");
+    assert!(remove_foreign_watermarks(&mut doc, &[1]).is_err());
+    assert!(remove_foreign_watermarks(&mut doc, &[usize::MAX]).is_err());
+}
+
+/// #4957: a watermark whose operators are split across content streams is cut out of both.
+#[test]
+fn a_foreign_watermark_across_content_streams_is_cut_out_of_each() {
+    let mut doc = page_of_streams(&["(a) Tj /Artifact <</Subtype /Watermark>> BDC q 0.5 g", "/Fm0 Do Q EMC (b) Tj"], "");
+    assert_eq!(remove_foreign_watermarks(&mut doc, &[0]).unwrap().removed, 1);
+    assert_eq!(streams(&doc, 0), ["(a) Tj ", " (b) Tj"]);
+}
+
+/// #4957: marked content does not save and restore the graphics state, so a watermark that
+/// changes state the rest of the page inherits is reported, not cut out (cutting it would repaint
+/// what follows in another colour, place or clip). One that keeps its changes inside its own
+/// `q … Q` is cut out whole.
+#[test]
+fn a_foreign_watermark_that_changes_state_for_what_follows_is_reported_not_removed() {
+    use crate::{Blocked, ForeignWatermarks};
+    let leaking = [
+        // The colour: the square after it is red before removal, black after.
+        "0 g /Artifact <</Subtype /Watermark>> BDC 1 0 0 rg 0 0 10 10 re f EMC 20 20 10 10 re f",
+        // The matrix, the clip, the font and the line width.
+        "/Artifact <</Subtype /Watermark>> BDC 1 0 0 1 50 50 cm EMC 0 0 5 5 re f",
+        "/Artifact <</Subtype /Watermark>> BDC 0 0 50 50 re W n EMC 0 0 100 100 re f",
+        "/Artifact <</Subtype /Watermark>> BDC BT /F1 40 Tf (x) Tj ET EMC BT (y) Tj ET",
+        "/Artifact <</Subtype /Watermark>> BDC 8 w EMC 0 0 m 9 9 l S",
+        // Operators that set state as a side effect: `"` sets the word and character spacing, `TD`
+        // the leading, and both outlast the text object they are in.
+        "BT /F1 12 Tf ET /Artifact <</Subtype /Watermark>> BDC BT 4 2 (WM) \" ET EMC BT 20 100 Td (A B) Tj ET",
+        "BT /F1 12 Tf ET /Artifact <</Subtype /Watermark>> BDC BT 0 -14 TD (WM) Tj ET EMC BT (a) ' ET",
+        // A text object or a path that begins or ends inside it.
+        "BT /Artifact <</Subtype /Watermark>> BDC (x) Tj ET EMC",
+        "0 0 m /Artifact <</Subtype /Watermark>> BDC 5 5 l S EMC",
+    ];
+    for content in leaking {
+        let mut doc = page_of_streams(&[content], "");
+        let result = remove_foreign_watermarks(&mut doc, &[0]).unwrap();
+        assert_eq!(result, ForeignWatermarks { removed: 0, blocked: vec![(0, Blocked::Unsafe)] }, "{content}");
+        assert_eq!(streams(&doc, 0), [content], "left exactly as it was: {content}");
+    }
+    // The same drawing with its changes inside its own q … Q goes, and what follows is untouched.
+    let scoped = [
+        "0 g /Artifact <</Subtype /Watermark>> BDC q 1 0 0 rg 1 0 0 1 5 5 cm 0 0 10 10 re f Q EMC 20 20 10 10 re f",
+        "/Artifact <</Subtype /Watermark>> BDC q BT /F1 40 Tf 0.7 g (x) Tj ET Q EMC BT (y) Tj ET",
+        "/Artifact <</Subtype /Watermark>> BDC q 0 0 50 50 re W n q 8 w 0 0 m 9 9 l S Q Q EMC",
+        "BT /F1 12 Tf ET /Artifact <</Subtype /Watermark>> BDC q BT 4 2 (WM) \" 0 -14 TD (x) Tj ET Q EMC BT 20 100 Td (A B) Tj ET",
+    ];
+    for content in scoped {
+        let mut doc = page_of_streams(&[content], "");
+        let result = remove_foreign_watermarks(&mut doc, &[0]).unwrap();
+        assert_eq!(result, ForeignWatermarks { removed: 1, blocked: vec![] }, "{content}");
+        assert!(!streams(&doc, 0)[0].contains("Watermark"), "{content}");
+    }
+    let mut doc = page_of_streams(&[scoped[0]], "");
+    remove_foreign_watermarks(&mut doc, &[0]).unwrap();
+    assert_eq!(streams(&doc, 0), ["0 g  20 20 10 10 re f"], "what is left is the body, byte for byte");
+}
+
+/// #4957: the watermark's subtype and its properties may be indirect objects.
+#[test]
+fn a_foreign_watermark_named_through_indirect_objects_is_found() {
+    use crate::ForeignWatermarks;
+    // Object 5 is the name Watermark; object 6 is a properties dictionary whose Subtype is object 5.
+    let content = "BT (A) Tj ET /Artifact /MC0 BDC q /Fm0 Do Q EMC BT (B) Tj ET";
+    let mut doc = page_of_streams_with(&[content], "/MC0 6 0 R", &["/Watermark", "<< /Subtype 5 0 R /Type /Pagination >>"]);
+    assert_eq!(remove_foreign_watermarks(&mut doc, &[0]).unwrap().removed, 1);
+    assert_eq!(streams(&doc, 0), ["BT (A) Tj ET  BT (B) Tj ET"]);
+    // A Subtype that is something else through a reference is not a watermark.
+    let mut doc = page_of_streams_with(&[content], "/MC0 6 0 R", &["/Header", "<< /Subtype 5 0 R /Type /Pagination >>"]);
+    assert_eq!(remove_foreign_watermarks(&mut doc, &[0]).unwrap(), ForeignWatermarks::default());
+}
+
+/// #4957: a page that could not be checked or cleaned is reported with the reason, the others are
+/// still cleaned, and a document with none says so.
+#[test]
+fn pages_a_foreign_watermark_could_not_be_removed_from_are_reported() {
+    use crate::{Blocked, ForeignWatermarks, foreign_watermarks_present};
+    let wm = "/Artifact <</Subtype /Watermark>> BDC q 0.5 g /Fm0 Do Q EMC";
+    // Three pages: a watermark that goes, content that is not there, and a watermark in content that
+    // can't be read with certainty.
+    let uncertain = format!("{wm} /Span BMC (x) Tj");
+    let mut objs: Vec<String> = vec![
+        "<< /Type /Catalog /Pages 2 0 R >>".into(),
+        "<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 /MediaBox [0 0 200 200] >>".into(),
+        "<< /Type /Page /Parent 2 0 R /Contents 6 0 R >>".into(),
+        "<< /Type /Page /Parent 2 0 R /Contents 99 0 R >>".into(),
+        "<< /Type /Page /Parent 2 0 R /Contents 8 0 R >>".into(),
+    ];
+    objs.push(format!("<< /Length {} >>\nstream\n{wm}\nendstream", wm.len()));
+    // Object 7 is not used: page 2's content is a reference to an object that is not there.
+    objs.push("<< >>".into());
+    objs.push(format!("<< /Length {} >>\nstream\n{uncertain}\nendstream", uncertain.len()));
+    let mut out = b"%PDF-1.7\n".to_vec();
+    let mut offs = Vec::new();
+    for (i, o) in objs.iter().enumerate() {
+        offs.push(out.len());
+        out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes());
+    }
+    let x = out.len();
+    out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
+    for o in offs {
+        out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
+    }
+    out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{x}\n%%EOF\n", objs.len() + 1).as_bytes());
+    let mut doc = Document::open(Arc::new(out)).unwrap();
+    // Detection: there is one, so Remove watermark is offered.
+    assert_eq!(foreign_watermarks_present(&doc), Some(true));
+    let result = remove_foreign_watermarks(&mut doc, &[0, 1, 2]).unwrap();
+    assert_eq!(result, ForeignWatermarks { removed: 1, blocked: vec![(1, Blocked::Unreadable), (2, Blocked::Uncertain)] });
+    assert!(!streams(&doc, 0)[0].contains("Watermark"), "the readable page was cleaned");
+    assert_eq!(
+        result.blocked.iter().map(|(_, why)| why.to_string()).collect::<Vec<_>>(),
+        ["its content can't be read", "its content can't be read with certainty"]
+    );
+
+    // No watermark anywhere: nothing removed, nothing blocked, and detection says no.
+    let mut plain = page_of_streams(&["BT (x) Tj ET", "q Q"], "");
+    assert_eq!(foreign_watermarks_present(&plain), Some(false));
+    assert_eq!(remove_foreign_watermarks(&mut plain, &[0]).unwrap(), ForeignWatermarks::default());
+    // Detection by a property alone, and an unreadable page makes it "maybe".
+    let named = page_of_streams(&["/Artifact /MC0 BDC EMC"], "/MC0 << /Subtype /Watermark >>");
+    assert_eq!(foreign_watermarks_present(&named), Some(true));
+}
+
+/// #4957: a watermark inside a text object or a path that the surrounding content owns would move
+/// that object's matrix or change that path when cut out, and `q … Q` restores neither: it is
+/// reported, whether or not the object is still open where the watermark ends.
+#[test]
+fn a_foreign_watermark_inside_an_open_text_object_or_path_is_not_cut_out() {
+    use crate::{Blocked, ForeignWatermarks};
+    for content in [
+        // The text matrix: the deleted Tj advanced it, so "Body" would shift left.
+        "BT /F1 12 Tf 20 200 Td /Artifact <</Subtype /Watermark>> BDC (WM) Tj EMC (Body) Tj ET",
+        "BT /F1 12 Tf /Artifact <</Subtype /Watermark>> BDC 30 0 Td EMC (Body) Tj ET",
+        // The current path: cutting the segment changes the shape that is stroked.
+        "0 0 m /Artifact <</Subtype /Watermark>> BDC 10 0 l EMC 10 10 l S",
+        "0 0 m /Artifact <</Subtype /Watermark>> BDC q Q EMC 10 10 l S",
+    ] {
+        let mut doc = page_of_streams(&[content], "");
+        let result = remove_foreign_watermarks(&mut doc, &[0]).unwrap();
+        assert_eq!(result, ForeignWatermarks { removed: 0, blocked: vec![(0, Blocked::Unsafe)] }, "{content}");
+        assert_eq!(streams(&doc, 0), [content], "{content}");
+    }
+}
+
+/// #4957: a name may be spelled with `#` escapes, so a document whose only watermark is spelled
+/// that way is still found, offered and cleaned.
+#[test]
+fn a_foreign_watermark_with_an_escaped_name_is_found() {
+    use crate::foreign_watermarks_present;
+    for content in [
+        "(x) Tj /Artifact <</Subtype /Water#6Dark /Type /Pagination>> BDC q Q EMC (y) Tj",
+        "(x) Tj /Arti#66act <</Subtype /Watermark>> BDC q Q EMC (y) Tj",
+        "(x) Tj /Artifact <</Sub#74ype /Watermark>> BDC q Q EMC (y) Tj",
+    ] {
+        let mut doc = page_of_streams(&[content], "");
+        assert_eq!(foreign_watermarks_present(&doc), Some(true), "{content}");
+        assert_eq!(remove_foreign_watermarks(&mut doc, &[0]).unwrap().removed, 1, "{content}");
+        assert_eq!(streams(&doc, 0), ["(x) Tj  (y) Tj"], "{content}");
+    }
+    // A `#` that is not a watermark: read in full, and nothing is found.
+    let hash = "(a #1 b) Tj /Artifact <</Subtype /Header>> BDC q Q EMC";
+    let doc = page_of_streams(&[hash], "");
+    assert_eq!(foreign_watermarks_present(&doc), Some(false));
+}
+
+/// #4957: the scan costs the same however deeply marked content is nested: a watermark scope with
+/// a thousand marked-content scopes inside it and a million state changes (some 4 MB) is read in
+/// moments, not in a billion steps.
+#[test]
+fn reading_deeply_nested_marked_content_with_many_state_changes_is_quick() {
+    use crate::{Blocked, ForeignWatermarks};
+    let depth = 1000;
+    let content =
+        format!("/Artifact <</Subtype /Watermark>> BDC {}{}{}EMC", "/Span BMC ".repeat(depth), "0 g ".repeat(1_000_000), "EMC ".repeat(depth));
+    let mut doc = page_of_streams(&[&content], "");
+    let started = std::time::Instant::now();
+    let result = remove_foreign_watermarks(&mut doc, &[0]).unwrap();
+    let took = started.elapsed();
+    // The state changes outlast the scope: reported, not cut.
+    assert_eq!(result, ForeignWatermarks { removed: 0, blocked: vec![(0, Blocked::Unsafe)] });
+    assert!(took < std::time::Duration::from_secs(5), "{took:?}");
+    eprintln!("deep nesting scan took {took:?}");
+}
diff --git a/crates/engine/src/commands.rs b/crates/engine/src/commands.rs
--- a/crates/engine/src/commands.rs
+++ b/crates/engine/src/commands.rs
@@ -38,6 +38,8 @@ pub enum Needs {
     HasRedactions,
     /// The document has page marks of this kind and allows changes.
     Marks(crate::MarkKind),
+    /// Remove watermark: PdfCraft's own, or one another editor wrote as an artifact (#4957).
+    WatermarkRemoval,
     /// The document's security may be changed (owner, or nothing restricted), or would be
     /// once its permissions password is entered.
     Security,
@@ -367,7 +369,7 @@ pub const COMMANDS: &[CommandSpec] = &[
     c("edit.header_footer.remove", "Remove header & footer", None, None, Marks(crate::MarkKind::HeaderFooter), "heading"),
     c("edit.watermark", "Add watermark…", None, None, Modification, "stamp"),
     c("edit.watermark.update", "Update watermark…", None, None, Marks(crate::MarkKind::Watermark), "stamp"),
-    c("edit.watermark.remove", "Remove watermark", None, None, Marks(crate::MarkKind::Watermark), "stamp"),
+    c("edit.watermark.remove", "Remove watermark", None, None, WatermarkRemoval, "stamp"),
     c("edit.background", "Add background…", None, None, Modification, "palette"),
     c("edit.background.update", "Update background…", None, None, Marks(crate::MarkKind::Background), "palette"),
     c("edit.background.remove", "Remove background", None, None, Marks(crate::MarkKind::Background), "palette"),
@@ -423,6 +425,9 @@ pub fn is_enabled(spec: &CommandSpec, session: &Session, active: Option<DocId>)
         Printing => doc.is_some_and(|d| d.allows_printing()),
         FillForms => doc.is_some_and(|d| d.allows_form_filling() && !d.form.is_empty()),
         Marks(k) => doc.is_some_and(|d| d.allows_modification() && d.marks().contains(&k)),
+        WatermarkRemoval => {
+            doc.is_some_and(|d| d.allows_modification() && (d.marks().contains(&crate::MarkKind::Watermark) || d.may_have_foreign_watermark()))
+        }
         HasComments => doc.is_some_and(|d| d.allows_modification() && !d.info.annotations.is_empty()),
         HasFields => doc.is_some_and(|d| d.allows_modification() && !d.form.is_empty()),
         HasRedactions => doc.is_some_and(|d| d.allows_modification() && d.redaction_marks() > 0),
diff --git a/crates/engine/src/document.rs b/crates/engine/src/document.rs
--- a/crates/engine/src/document.rs
+++ b/crates/engine/src/document.rs
@@ -37,6 +37,9 @@ pub struct Document {
     /// Interactive form fields of the current state (empty without a form).
     pub form: Arc<Vec<pdfcraft_forms::Field>>,
     pub(crate) marker_scan: OnceLock<pdfcraft_edit::MarkerScan>,
+    /// Whether a watermark another editor wrote is there (`None`: could not tell), looked for the
+    /// first time it is asked, and again after the content changes.
+    pub(crate) foreign_watermark_scan: OnceLock<Option<bool>>,
     pub(crate) links: OnceLock<Vec<pdfcraft_annot::links::LinkItem>>,
     /// Signature fields, validated against the session's trust store (Signatures panel).
     pub signatures: Arc<Vec<SignatureInfo>>,
@@ -359,6 +362,14 @@ impl Document {
         self.info.annotations.iter().filter(|a| a.subtype == "Redact").count()
     }
 
+    /// Whether the document may have a watermark another editor wrote as an artifact: it has one,
+    /// or it could not be checked (a very large document, an unreadable page).
+    pub fn may_have_foreign_watermark(&self) -> bool {
+        self.foreign_watermark_scan
+            .get_or_init(|| self.editor.as_ref().and_then(|e| pdfcraft_edit::foreign_watermarks_present(&e.cos)))
+            .unwrap_or(true)
+    }
+
     /// Page marks present (headers and footers, watermarks, backgrounds), for Update/Remove.
     pub fn marks(&self) -> &[MarkKind] {
         &self
diff --git a/crates/engine/src/edit/run.rs b/crates/engine/src/edit/run.rs
--- a/crates/engine/src/edit/run.rs
+++ b/crates/engine/src/edit/run.rs
@@ -538,7 +538,19 @@ pub(crate) fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut E
         }
         Edit::RemoveMarks { kind } => {
             let n = pdfcraft_model::pages(doc).len();
-            if pdfcraft_edit::remove_marks(doc, &(0..n).collect::<Vec<_>>(), *kind)? == 0 {
+            let pages: Vec<usize> = (0..n).collect();
+            let mut removed = pdfcraft_edit::remove_marks(doc, &pages, *kind)?;
+            // Watermarks another editor marked as artifacts go with PdfCraft's own (#4957). A page
+            // one could not be removed from, or checked on, stops the edit with the reason (nothing
+            // changes): removing some and saying nothing about the rest would be wrong.
+            if *kind == MarkKind::Watermark {
+                let foreign = pdfcraft_edit::remove_foreign_watermarks(doc, &pages)?;
+                if !foreign.blocked.is_empty() {
+                    return Err(EditError::Edit(pdfcraft_edit::EditError::Invalid(blocked_watermarks(&foreign.blocked))));
+                }
+                removed += foreign.removed;
+            }
+            if removed == 0 {
                 return Err(EditError::Edit(pdfcraft_edit::EditError::Invalid("there is nothing to remove".into())));
             }
         }
@@ -694,3 +706,13 @@ pub(crate) fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut E
     }
     Ok(())
 }
+
+/// "Remove watermark" refused: the pages a watermark could not be removed from, and why.
+fn blocked_watermarks(blocked: &[(usize, pdfcraft_edit::Blocked)]) -> String {
+    const SHOWN: usize = 5;
+    let mut parts: Vec<String> = blocked.iter().take(SHOWN).map(|(page, why)| format!("page {}: {why}", page.saturating_add(1))).collect();
+    if blocked.len() > SHOWN {
+        parts.push(format!("and {} more pages", blocked.len() - SHOWN));
+    }
+    format!("a watermark another editor made could not be removed from every page ({}); nothing was changed", parts.join("; "))
+}
diff --git a/crates/engine/src/open.rs b/crates/engine/src/open.rs
--- a/crates/engine/src/open.rs
+++ b/crates/engine/src/open.rs
@@ -337,6 +337,7 @@ impl Session {
             read_only_reason,
             form: Arc::new(form),
             marker_scan: OnceLock::new(),
+            foreign_watermark_scan: OnceLock::new(),
             links: OnceLock::new(),
             signatures: Arc::new(signatures),
             trust: trust.clone(),
diff --git a/crates/engine/src/session/editing.rs b/crates/engine/src/session/editing.rs
--- a/crates/engine/src/session/editing.rs
+++ b/crates/engine/src/session/editing.rs
@@ -425,6 +425,7 @@ impl Session {
         let (touches_added, touches_marks) = Self::content_refresh_lists(edit);
         if touches_added || touches_marks {
             doc.marker_scan = OnceLock::new();
+            doc.foreign_watermark_scan = OnceLock::new();
         }
         doc.info.file_size = bytes.len();
         let signatures = if doc.signatures.is_empty() { Vec::new() } else { signatures_of(&editor.cos, &bytes, &doc.trust, &doc.sig_cache) };
@@ -507,6 +508,7 @@ impl Session {
         doc.form = Arc::new(form);
         if invalidate_markers {
             doc.marker_scan = OnceLock::new();
+            doc.foreign_watermark_scan = OnceLock::new();
         }
         if invalidate_links {
             doc.links = OnceLock::new();
diff --git a/crates/ui-egui/tests/it/marks.rs b/crates/ui-egui/tests/it/marks.rs
--- a/crates/ui-egui/tests/it/marks.rs
+++ b/crates/ui-egui/tests/it/marks.rs
@@ -328,3 +328,50 @@ fn page_range_from_one_to_one_selects_only_the_first_page() {
     assert_eq!(corner(&mut r, 0), vec![255, 255, 217, 255], "page 1 has the background");
     assert_eq!(corner(&mut r, 1), vec![255, 255, 255, 255], "page 2 does not");
 }
+
+/// A one-page PDF with a watermark another editor marked as an artifact (and some body text).
+fn foreign_watermark_pdf() -> Vec<u8> {
+    let content = "BT /F1 18 Tf 20 250 Td (Body text) Tj ET\n/Artifact <</Subtype /Watermark /Type /Pagination>> BDC q BT /F1 40 Tf 20 100 Td (DRAFT) Tj ET Q EMC";
+    format!(
+        "%PDF-1.7
+1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
+2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 300 400] >> endobj
+3 0 obj << /Type /Page /Parent 2 0 R /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >> endobj
+4 0 obj << /Length {} >>
+stream
+{content}
+endstream
+endobj
+5 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj
+trailer << /Root 1 0 R >>
+%%EOF",
+        content.len()
+    )
+    .into_bytes()
+}
+
+/// #4957: Remove watermark is offered for a document whose only watermark was made by another
+/// editor, removes it, and is no longer offered; undo brings both back. A document without one
+/// still has it disabled.
+#[test]
+fn remove_watermark_is_offered_for_a_watermark_another_editor_made() {
+    let may = |h: &Harness<'static, PdfCraftApp>| {
+        let s = h.state();
+        s.session.get(s.views[0].id).unwrap().may_have_foreign_watermark()
+    };
+    let mut plain = harness();
+    assert!(!may(&plain));
+    assert!(!plain.state_mut().execute("edit.watermark.remove"), "nothing to remove in a document without one");
+
+    let mut h = harness_with(foreign_watermark_pdf());
+    assert!(marks(&h).is_empty(), "it is not one of PdfCraft's own marks");
+    assert!(may(&h));
+    assert!(h.state_mut().execute("edit.watermark.remove"), "Remove watermark is offered and runs");
+    h.run_steps(3);
+    assert!(!may(&h), "and it is gone");
+    assert!(!h.state_mut().execute("edit.watermark.remove"), "so it is offered no more");
+    h.state_mut().undo();
+    h.run_steps(3);
+    assert!(may(&h), "undo brings the watermark back, and the command with it");
+    assert!(h.state_mut().execute("edit.watermark.remove"));
+}
diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml
--- a/parity/acrobat-features.toml
+++ b/parity/acrobat-features.toml
@@ -2507,8 +2507,8 @@ milestone = "M7"
 status = "shipped"
 commands = ["edit.watermark.update", "edit.watermark.remove"]
 tools = ["doc_watermark", "doc_remove_marks"]
-evidence = ["crates/edit/src/tests.rs::replace_and_remove_restore_the_original_content", "crates/engine/src/tests.rs::headers_footers_watermarks_and_backgrounds_show_update_and_remove"]
-notes = ""
+evidence = ["crates/edit/src/tests.rs::replace_and_remove_restore_the_original_content", "crates/engine/src/tests.rs::headers_footers_watermarks_and_backgrounds_show_update_and_remove", "crates/edit/src/tests.rs::foreign_watermark_artifacts_are_cut_out_and_nothing_else_changes", "crates/edit/src/tests.rs::foreign_watermark_removal_leaves_everything_it_should", "crates/edit/src/tests.rs::a_foreign_watermark_across_content_streams_is_cut_out_of_each", "crates/automation/tests/it/automation.rs::remove_marks_takes_out_watermarks_other_editors_marked_as_artifacts"]
+notes = ". Remove watermark also takes out watermarks another editor wrote as artifacts (#4957): page content between /Artifact BDC and EMC whose properties, inline or a name in /Properties (the dictionary and its Subtype may be indirect), say /Subtype /Watermark (ISO 32000-2 14.8.2.2), cut out byte for byte, and the command is offered for a document that has only such a watermark. Marked content does not save the graphics state, so a watermark that changes the colour, matrix, clip, font or line state outside its own q and Q, or sits inside a text object or a path that the content around it owns (cutting it would move that text or change that path), or opens or closes one, is not cut out; names spelled with # escapes are read as their decoded names; neither is one in a page whose marked-content scopes do not pair up. A page where that happens, or whose content cannot be read, stops the whole removal with an error naming the page and the reason, and nothing is changed. Missing: Update (replace) does not replace such a watermark, the dialog does not read its text or settings back, and a watermark an editor wrote another way (a layer-only form, an annotation, a page-wide image with no artifact marking) is not found; the form, font or image a removed watermark drew with stays in the resources, unused; a very large document (over 5,000 pages) offers Remove watermark without looking"
 
 [[feature]]
 id = "edit.watermark-appearance"
