mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
fix(edit): refuse a mark on top of over-deep content before changing the page
This commit is contained in:
@@ -1050,6 +1050,9 @@ fn write(doc: &mut Document, page: usize, c: &Content, obj: Option<ObjRef>) -> R
|
||||
}
|
||||
let taken = pres.get(b"Font").map(|f| doc.resolve(f)).and_then(|f| f.as_dict().cloned()).unwrap_or_default();
|
||||
let (content, res) = draw(doc, c, view, &taken, obj.is_some())?;
|
||||
if obj.is_none() {
|
||||
crate::check_on_top(doc, p)?;
|
||||
}
|
||||
for (k, v) in res.iter() {
|
||||
let mut sub = pres.get(k).map(|s| doc.resolve(s)).and_then(|s| s.as_dict().cloned()).unwrap_or_default();
|
||||
for (n2, o) in v.as_dict().into_iter().flat_map(|d| d.iter()) {
|
||||
|
||||
@@ -257,6 +257,7 @@ fn flatten_which(doc: &mut Document, pages: &[usize], which: Which) -> Result<us
|
||||
.map(|(_, e)| e.clone())
|
||||
.collect();
|
||||
if !content.is_empty() {
|
||||
crate::check_on_top(doc, &page)?;
|
||||
// The page's own resources, with the appearances as XObjects.
|
||||
let mut res = page.dict.get(b"Resources").map(|r| doc.resolve(r)).and_then(|r| r.as_dict().cloned()).unwrap_or_default();
|
||||
let mut xo = res.get(b"XObject").map(|x| doc.resolve(x)).and_then(|x| x.as_dict().cloned()).unwrap_or_default();
|
||||
|
||||
+30
-3
@@ -631,9 +631,7 @@ fn place_tagged(doc: &mut Document, page: &pdfcraft_model::Page, tag: &str, cont
|
||||
// closing it changes nothing already drawn.
|
||||
Isolation::Unknown { unclosed, marked } => (behind_marks < list.len()).then_some((behind_marks, unclosed, marked)),
|
||||
// Content left inside an optional-content sequence it cannot close could be hidden.
|
||||
Isolation::TooDeep => {
|
||||
return Err(EditError::Invalid("the page's marked content is nested too deeply to add anything on top of it".into()));
|
||||
}
|
||||
Isolation::TooDeep => return Err(too_deep_on_top()),
|
||||
}
|
||||
};
|
||||
let mark_ref = doc.add(Object::Stream(Stream::flate(tagged(tag), &content)));
|
||||
@@ -661,6 +659,26 @@ fn place_tagged(doc: &mut Document, page: &pdfcraft_model::Page, tag: &str, cont
|
||||
Ok(mark_ref)
|
||||
}
|
||||
|
||||
fn too_deep_on_top() -> EditError {
|
||||
EditError::Invalid("the page's marked content is nested too deeply to add anything on top of it".into())
|
||||
}
|
||||
|
||||
/// The refusal [`place_tagged`] gives for a mark on top of `page`, checked before an edit changes
|
||||
/// anything (removing old marks, adding resources) so a refused edit leaves the document as it was.
|
||||
fn check_on_top(doc: &Document, page: &pdfcraft_model::Page) -> Result<(), EditError> {
|
||||
let obj = doc.get(page.obj);
|
||||
let list = match obj.as_dict().and_then(|d| d.get(b"Contents")) {
|
||||
None => return Ok(()),
|
||||
Some(Object::Ref(r)) => doc.get(*r).as_array().cloned().unwrap_or_else(|| vec![Object::Ref(*r)]),
|
||||
Some(Object::Array(a)) => a.clone(),
|
||||
Some(other) => vec![other.clone()],
|
||||
};
|
||||
match isolation(doc, &list) {
|
||||
Isolation::TooDeep => Err(too_deep_on_top()),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Put content tagged `tag` first inside the q/Q wrapper, so it applies to the page's original
|
||||
/// content and nothing else (a header's shrink). A page without a wrapper has no original content
|
||||
/// on it and is left alone.
|
||||
@@ -680,6 +698,7 @@ fn place_inside_wrapper(doc: &mut Document, page: &pdfcraft_model::Page, tag: &s
|
||||
pub fn stamp(doc: &mut Document, page: usize, tag: &str, content: Vec<u8>) -> Result<ObjRef, EditError> {
|
||||
let all = page_list(doc);
|
||||
check(&[page], all.len())?;
|
||||
check_on_top(doc, &all[page])?;
|
||||
add_resources(doc, &all[page], None, Some(&content))?;
|
||||
let p = page_list(doc).swap_remove(page);
|
||||
place_tagged(doc, &p, tag, content, false)
|
||||
@@ -759,6 +778,9 @@ pub fn add_header_footer(doc: &mut Document, pages: &[usize], hf: &HeaderFooter,
|
||||
.iter()
|
||||
.map(|&i| hf.shrink.then(|| shrink_matrix(hf, all[i].display_size(doc), all[i].view_matrix(doc))).transpose())
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
for &i in pages {
|
||||
check_on_top(doc, &all[i])?;
|
||||
}
|
||||
if replace {
|
||||
remove_marks(doc, pages, MarkKind::HeaderFooter)?;
|
||||
}
|
||||
@@ -847,6 +869,11 @@ pub fn add_watermark(doc: &mut Document, pages: &[usize], wm: &Watermark, replac
|
||||
{
|
||||
return Err(EditError::Invalid("invalid watermark settings".into()));
|
||||
}
|
||||
if !wm.behind {
|
||||
for &i in pages {
|
||||
check_on_top(doc, &all[i])?;
|
||||
}
|
||||
}
|
||||
if replace {
|
||||
remove_marks(doc, pages, MarkKind::Watermark)?;
|
||||
}
|
||||
|
||||
@@ -131,3 +131,29 @@ fn depth_budget_bounds_emitted_closures_and_overflow_is_refused_on_top() {
|
||||
place_tagged(&mut doc, &page, MarkKind::Background.tag(), b"q Q\n".to_vec(), true).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks refused on top of content nested past the depth budget leave the page as it was: old
|
||||
/// marks are not removed and no resources are added before the refusal.
|
||||
#[test]
|
||||
fn a_mark_refused_on_top_changes_nothing() {
|
||||
let mut over = b"/Artifact BMC\n".repeat(ISOLATION_MAX_DEPTH);
|
||||
over.extend_from_slice(b"/Artifact BMC\n");
|
||||
let mut doc = fixture(&[&over]);
|
||||
let behind = Watermark { text: "OLD".into(), behind: true, ..Watermark::default() };
|
||||
add_watermark(&mut doc, &[0], &behind, false).unwrap();
|
||||
let page = page_list(&doc).remove(0).obj;
|
||||
let before = (*doc.get(page)).clone();
|
||||
let on_top = Watermark { text: "NEW".into(), ..Watermark::default() };
|
||||
assert!(matches!(add_watermark(&mut doc, &[0], &on_top, true), Err(EditError::Invalid(_))));
|
||||
assert_eq!((*doc.get(page)).clone(), before, "the old watermark stays");
|
||||
let hf = HeaderFooter {
|
||||
text: [String::new(), "Top".into(), String::new(), String::new(), String::new(), String::new()],
|
||||
font: added::Family::Times,
|
||||
..HeaderFooter::default()
|
||||
};
|
||||
assert!(matches!(add_header_footer(&mut doc, &[0], &hf, true, &Context::default()), Err(EditError::Invalid(_))));
|
||||
assert_eq!((*doc.get(page)).clone(), before, "no fonts are added");
|
||||
let mut stamped = doc.clone();
|
||||
assert!(matches!(stamp(&mut stamped, 0, "Probe", b"/PCTimes 9 Tf\n".to_vec()), Err(EditError::Invalid(_))));
|
||||
assert_eq!((*stamped.get(page)).clone(), before, "no fonts are added");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user