mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 09:51:18 -06:00
fix(edit): refuse arrange across crossed scope boundaries
Require both graphics and marked-content boundaries to balance inside source, destination and removed spans. Regression fails before the guard; all 408 edit tests, scoped clippy, fmt, layers and assets pass on GGEO-AI02. Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
This commit is contained in:
co-authored by
Codex GPT-6.1
parent
eb0d5e64c7
commit
1a2f7b2a34
@@ -976,6 +976,30 @@ fn target_destination_unit<'a>(
|
||||
dest.map(|u| (u, forward)).ok_or_else(refused)
|
||||
}
|
||||
|
||||
/// A moved unit must not borrow a graphics or marked-content boundary from its neighbours.
|
||||
fn balanced_arrange_span(ops: &[Op], range: std::ops::Range<usize>) -> bool {
|
||||
let Some(slice) = ops.get(range) else { return false };
|
||||
let (mut graphics, mut marked) = (0usize, 0usize);
|
||||
for op in slice {
|
||||
let depth = match op.op.as_slice() {
|
||||
b"q" => {
|
||||
graphics = graphics.saturating_add(1);
|
||||
continue;
|
||||
}
|
||||
b"BDC" | b"BMC" => {
|
||||
marked = marked.saturating_add(1);
|
||||
continue;
|
||||
}
|
||||
b"Q" => &mut graphics,
|
||||
b"EMC" => &mut marked,
|
||||
_ => continue,
|
||||
};
|
||||
let Some(next) = depth.checked_sub(1) else { return false };
|
||||
*depth = next;
|
||||
}
|
||||
graphics == 0 && marked == 0
|
||||
}
|
||||
|
||||
/// What Arrange takes out: the byte span, the op where the image's own scope state starts, and the
|
||||
/// ops `start..end` removed. That is the image's own `q … Q` scope inside its unit, else the `Do`
|
||||
/// alone when the unit paints something else too, else the whole unit.
|
||||
@@ -999,6 +1023,9 @@ fn determine_arrange_spans(
|
||||
_ if marked_unit => (target_unit.start, target_unit.start.saturating_add(1).min(target), unit_last),
|
||||
_ => (target_unit.start, target_unit.start, unit_last),
|
||||
};
|
||||
if !balanced_arrange_span(ops, first..last.saturating_add(1)) {
|
||||
return Err(EditError::Invalid("cannot arrange an image across graphics or marked-content boundaries".into()));
|
||||
}
|
||||
let missing = || EditError::Invalid("the image content unit could not be located".into());
|
||||
let start = ops.get(first).ok_or_else(missing)?.span.start;
|
||||
let end = ops.get(last).ok_or_else(missing)?.span.end;
|
||||
@@ -1149,6 +1176,9 @@ fn arrange_image(
|
||||
}
|
||||
|
||||
let (dest_unit, insert_after) = target_destination_unit(&units, target_idx, target_unit, order, has_other_paint)?;
|
||||
if !balanced_arrange_span(&ops, target_unit.clone()) || !balanced_arrange_span(&ops, dest_unit.clone()) {
|
||||
return Err(EditError::Invalid("cannot arrange an image across graphics or marked-content boundaries".into()));
|
||||
}
|
||||
|
||||
// A marked-content sequence that holds other content cannot be split without generating
|
||||
// a new structure element and MCID, so we refuse to rearrange it to avoid leaving the image untagged.
|
||||
|
||||
@@ -9938,3 +9938,17 @@ fn styled_existing_justification_is_refused_instead_of_silently_left_aligning()
|
||||
assert_eq!(page_content_bytes(&doc, 0), before);
|
||||
assert_eq!(page_content_bytes(&reopen(&doc), 0), before);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn image_arrange_refuses_crossed_scope_boundaries_without_changes() {
|
||||
for content in [
|
||||
"/P <</MCID 0>> BDC q 10 0 0 10 0 0 cm /Im0 Do EMC Q /Im0 Do",
|
||||
"q /P <</MCID 0>> BDC /Im0 Do Q EMC /Im0 Do",
|
||||
] {
|
||||
let mut doc = image_page_with(content);
|
||||
let before = page_content_bytes(&doc, 0);
|
||||
assert!(arrange(&mut doc, 0, 0, images::ArrangeOrder::BringToFront).is_err(), "{content}");
|
||||
assert_eq!(page_content_bytes(&doc, 0), before);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user