mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
Rewriting a paragraph also removed every line overlapping its box by more than half, to clear fake-bold and repeated copies. Each drag is its own edit, so after dropping one paragraph on another and dragging it on, the covered paragraph was taken for a copy and deleted from the content stream. A copy now also has to show the same text. The same check is used when replacing a line, so a distinct line drawn close enough to overlap (#1316) is no longer removed either.
This commit is contained in:
1 parent
a0575f2478
commit
5bf6d17538
3 files changed
+32
-17
No files matched your search
@@ -1824,6 +1824,21 @@ fn a_line_drawn_twice_is_replaced_everywhere() {
|
||||
assert_eq!(lines.iter().map(|l| l.text.as_str()).collect::<Vec<_>>(), ["Hello there", "world"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_paragraph_dragged_across_another_leaves_it_alone() {
|
||||
// #1368: each drag is its own edit. Dropping Alpha on Bravo, then dragging it on, treated
|
||||
// Bravo (now under Alpha) as a copy of Alpha and deleted it.
|
||||
let mut doc = text_page("BT /F1 12 Tf 100 700 Td (Alpha line) Tj ET BT /F1 12 Tf 100 500 Td (Bravo line) Tj ET");
|
||||
let down = text::BlockStyle { offset: Some([0.0, -200.0]), ..Default::default() };
|
||||
text::rewrite_block(&mut doc, 0, 0, None, &down).unwrap();
|
||||
let alpha = text::text_blocks(&doc, 0).unwrap().iter().position(|b| b.text == "Alpha line").unwrap();
|
||||
text::rewrite_block(&mut doc, 0, alpha, None, &down).unwrap();
|
||||
let doc = reopen(&doc);
|
||||
let mut texts: Vec<_> = text::text_lines(&doc, 0).unwrap().into_iter().map(|l| l.text).collect();
|
||||
texts.sort();
|
||||
assert_eq!(texts, ["Alpha line", "Bravo line"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paragraphs_move_and_rewrap_to_a_new_width() {
|
||||
let para = "BT /F1 10 Tf 12 TL 100 700 Td (One two three four five six seven) Tj T* (eight nine ten eleven twelve) Tj ET";
|
||||
|
||||
+16
-15
@@ -1434,7 +1434,7 @@ pub(crate) fn replace_line_with_limit(doc: &mut Document, page: usize, line: usi
|
||||
}
|
||||
// Rebuild: the line's first operator becomes the replacement, its others go. Copies of the
|
||||
// line drawn in the same area go too, so the replacement is all that shows.
|
||||
let drops = coincident_ops(&lines, std::slice::from_ref(&target.rect));
|
||||
let drops = coincident_ops(&lines, &[&target]);
|
||||
let new_data = splice(&pieces, &ops, |i| if i == first { (std::mem::take(&mut replacement), false) } else { (Vec::new(), !drops.contains(&i)) });
|
||||
let contents = rewritten_contents(doc, &streams, new_data);
|
||||
let page_ref = p.obj;
|
||||
@@ -1455,19 +1455,21 @@ fn font_size_before(ops: &[Op], at: usize) -> Option<f64> {
|
||||
ops[..at].iter().rev().find(|o| o.is("Tf")).and_then(|o| o.num(1))
|
||||
}
|
||||
|
||||
/// The operators of every line drawn in the same area as one of `rects`. Documents sometimes
|
||||
/// The operators of every copy of one of `members`. Documents sometimes
|
||||
/// draw a line more than once (fake bold, an invisible text layer), and a surviving copy would
|
||||
/// show the old text under the replaced line. Copies overlap a member's rect by more than half of
|
||||
/// the smaller rect; neighbouring lines share no area.
|
||||
fn coincident_ops(lines: &[TextLine], rects: &[[f64; 4]]) -> std::collections::HashSet<usize> {
|
||||
/// show the old text under the replaced line. Copies show the same text and overlap a member's
|
||||
/// rect by more than half of the smaller rect; neighbouring lines share no area, and other text
|
||||
/// that merely overlaps (a paragraph dragged across another, #1368) is not a copy.
|
||||
fn coincident_ops(lines: &[TextLine], members: &[&TextLine]) -> std::collections::HashSet<usize> {
|
||||
let area = |r: [f64; 4]| ((r[2] - r[0]) * (r[3] - r[1])).max(0.0);
|
||||
let mut drop = std::collections::HashSet::new();
|
||||
for l in lines {
|
||||
let covered = rects.iter().any(|m| {
|
||||
let ix = (m[2].min(l.rect[2]) - m[0].max(l.rect[0])).max(0.0);
|
||||
let iy = (m[3].min(l.rect[3]) - m[1].max(l.rect[1])).max(0.0);
|
||||
let small = area(*m).min(area(l.rect));
|
||||
small > 0.0 && ix * iy / small > 0.5
|
||||
let covered = members.iter().any(|m| {
|
||||
let r = m.rect;
|
||||
let ix = (r[2].min(l.rect[2]) - r[0].max(l.rect[0])).max(0.0);
|
||||
let iy = (r[3].min(l.rect[3]) - r[1].max(l.rect[1])).max(0.0);
|
||||
let small = area(r).min(area(l.rect));
|
||||
small > 0.0 && ix * iy / small > 0.5 && l.text.trim() == m.text.trim()
|
||||
});
|
||||
if covered {
|
||||
drop.extend(l.ops.iter().copied());
|
||||
@@ -1489,9 +1491,9 @@ pub fn clear_block(doc: &mut Document, page: usize, block: usize) -> Result<(),
|
||||
let lines = text_lines(doc, page)?;
|
||||
let blocks = group_blocks(&lines);
|
||||
let target = blocks.get(block).ok_or_else(|| EditError::Invalid("the paragraph no longer exists".into()))?;
|
||||
let rects: Vec<_> = target.lines.iter().filter_map(|i| lines.get(*i)).map(|l| l.rect).collect();
|
||||
let mut drops = coincident_ops(&lines, &rects);
|
||||
drops.extend(target.lines.iter().filter_map(|i| lines.get(*i)).flat_map(|l| l.ops.iter().copied()));
|
||||
let members: Vec<_> = target.lines.iter().filter_map(|i| lines.get(*i)).collect();
|
||||
let mut drops = coincident_ops(&lines, &members);
|
||||
drops.extend(members.iter().flat_map(|l| l.ops.iter().copied()));
|
||||
let p = page_dict(doc, page)?;
|
||||
let res = p.dict.get(b"Resources").and_then(|r| doc.resolve(r).as_dict().cloned()).unwrap_or_default();
|
||||
let fonts = res.get(b"Font").and_then(|r| doc.resolve(r).as_dict().cloned()).unwrap_or_default();
|
||||
@@ -1873,8 +1875,7 @@ pub(crate) fn rewrite_block_with_limit(
|
||||
block_ops.push(Op::new("Q", vec![]));
|
||||
// The paragraph's operators go, and so does anything drawn in the same area (a fake-bold
|
||||
// second copy, an invisible text layer): the new text is all that may show.
|
||||
let member_rects: Vec<[f64; 4]> = members.iter().map(|l| l.rect).collect();
|
||||
let mut drop = coincident_ops(&lines, &member_rects);
|
||||
let mut drop = coincident_ops(&lines, &members);
|
||||
drop.extend(members.iter().flat_map(|l| l.ops.iter().copied()));
|
||||
// Text shown earlier in the same text object stays first: the paragraph then goes where it
|
||||
// was, between two halves of the text object, so it keeps its place in reading order.
|
||||
|
||||
@@ -668,8 +668,7 @@ pub(super) fn rewrite_styled_block(
|
||||
let pieces = Pieces::join(&streams.iter().map(|(_, d)| d.as_slice()).collect::<Vec<_>>());
|
||||
let ops = pieces.parse().ops;
|
||||
let members: Vec<_> = block.lines.iter().filter_map(|i| lines.get(*i)).collect();
|
||||
let rects: Vec<_> = members.iter().map(|l| l.rect).collect();
|
||||
let mut drop = coincident_ops(lines, &rects);
|
||||
let mut drop = coincident_ops(lines, &members);
|
||||
drop.extend(members.iter().flat_map(|l| l.ops.iter().copied()));
|
||||
let o = &first.origin;
|
||||
let start = first.ops.first().copied().unwrap_or(o.bt_op);
|
||||
|
||||
Reference in new issue
Block a user