diff --git a/crates/xfdf/src/appearance.rs b/crates/xfdf/src/appearance.rs
index d8cbddb4ad..80105c715c 100644
--- a/crates/xfdf/src/appearance.rs
+++ b/crates/xfdf/src/appearance.rs
@@ -120,6 +120,12 @@ impl Writer<'_> {
}
self.out.push_str("\n");
}
+ // A page (or the page tree) reached from the appearance is the source document's, not
+ // the comment's: written in place it would carry every page along.
+ Object::Dict(d) if matches!(d.name(b"Type"), Some(b"Page" | b"Pages")) => self.out.push_str(&format!(
+ "
+"
+ )),
Object::Dict(d) => self.dict(key, d, depth)?,
Object::Stream(s) => {
// The data plus its hexadecimal form must fit what is left of the budget.
diff --git a/crates/xfdf/src/tests_format.rs b/crates/xfdf/src/tests_format.rs
index 83e91983e2..da0aa932f3 100644
--- a/crates/xfdf/src/tests_format.rs
+++ b/crates/xfdf/src/tests_format.rs
@@ -567,6 +567,23 @@ fn appearance_cycles_and_strings_survive_export() {
assert!(sub_dict(&dst, &res, b"Pattern").is_empty());
}
+#[test]
+fn an_appearance_that_names_its_page_does_not_carry_the_page_along() {
+ let mut src = pages(1);
+ image_stamp(&mut src, "pg", "Stamp");
+ let d = named(&src, "pg");
+ let n = sub_dict(&src, &d, b"AP").get(b"N").and_then(Object::as_ref).unwrap();
+ let page = page_ref(&src, 0);
+ let Object::Stream(mut form) = (*src.get(n)).clone() else { panic!("no form") };
+ form.dict.set(b"PieceInfo".to_vec(), Object::Ref(page));
+ src.set(n, Object::Stream(form));
+ let xml = export_xfdf(&src, true, false, "src.pdf");
+ let b64: String = xml.split("").nth(1).unwrap().split("").next().unwrap().split_whitespace().collect();
+ let inner = String::from_utf8(base64::engine::general_purpose::STANDARD.decode(b64).unwrap()).unwrap();
+ assert!(inner.contains(r#""#), "{inner}");
+ assert!(!inner.contains("Kids") && !inner.contains("MediaBox"));
+}
+
#[test]
fn fdf_carries_appearances_with_their_resources() {
let mut src = pages(1);