diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index f8a5d67e8..013b8d69a 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -728,3 +728,51 @@ fn paragraphs_move_and_rewrap_to_a_new_width() { text::rewrite_block(&mut doc, 0, 0, None, &text::BlockStyle { width: Some(0.0), ..Default::default() }).unwrap(); assert_eq!(text::text_blocks(&reopen(&doc), 0).unwrap().iter().map(|b| b.text.split_whitespace().count()).sum::(), 12); } + +/// A page split into several content streams, as AutoCAD writes them: the first scales the +/// page (`0.12 0 0 0.12 0 0 cm`, no `q`), the text comes in a later one. +fn split_page(first: &str, second: &str) -> Document { + let objs: Vec = vec![ + "<< /Type /Catalog /Pages 2 0 R >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 600 800] /Contents [4 0 R 5 0 R] /Resources << /Font << /F1 6 0 R >> >> >>".into(), + format!("<< /Length {} >>\nstream\n{first}\nendstream", first.len()), + format!("<< /Length {} >>\nstream\n{second}\nendstream", second.len()), + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".into(), + ]; + 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() +} + +#[test] +fn the_graphics_state_carries_over_between_content_streams() { + let close = |a: f64, b: f64| (a - b).abs() < 0.5; + // Text at (1000, 2000) in a space scaled by 0.12: on the page at (120, 240), 12 pt. + let mut doc = split_page("0.12 0 0 0.12 0 0 cm", "BT /F1 100 Tf 1000 2000 Td (Site plan) Tj ET"); + let lines = text::text_lines(&doc, 0).unwrap(); + assert_eq!(lines.len(), 1, "{lines:?}"); + let l = &lines[0]; + assert!(close(l.rect[0], 120.0) && l.rect[1] > 230.0 && l.rect[3] < 260.0, "{:?}", l.rect); + assert!(close(l.size, 12.0), "{}", l.size); + // Editing keeps the paragraph where it was, at its size; moving it moves it in page space. + text::rewrite_block(&mut doc, 0, 0, Some("Site plan, rev. B"), &text::BlockStyle { offset: Some([10.0, -20.0]), ..Default::default() }).unwrap(); + let after = text::text_blocks(&reopen(&doc), 0).unwrap(); + assert_eq!(after[0].text, "Site plan, rev. B"); + assert!(close(after[0].rect[0], 130.0) && close(after[0].rect[1], l.rect[1] - 20.0), "{:?}", after[0].rect); + assert!(close(after[0].size, 12.0), "{}", after[0].size); + // A `q` left open in one stream and closed in the next still restores the state. + let doc = split_page("q 0.5 0 0 0.5 0 0 cm", "Q BT /F1 10 Tf 100 100 Td (Footpath) Tj ET"); + let l = &text::text_lines(&doc, 0).unwrap()[0]; + assert!(close(l.rect[0], 100.0) && close(l.size, 10.0), "{l:?}"); +} diff --git a/crates/edit/src/text.rs b/crates/edit/src/text.rs index 2454be81a..75960b8c9 100644 --- a/crates/edit/src/text.rs +++ b/crates/edit/src/text.rs @@ -219,21 +219,36 @@ struct Shown { decodable: bool, } -fn interpret(doc: &Document, ops: &[Op], fonts_res: &Dict, cache: &mut HashMap, Rc>) -> Vec { +/// The graphics state carried from one of a page's content streams to the next: the streams +/// are one stream in pieces (ยง7.8.2), so a `cm` (AutoCAD scales the whole page in the first +/// stream), an unbalanced `q`, the font and the colour still apply in the streams after it. +struct Carry { + ts: Ts, + stack: Vec, +} + +impl Carry { + fn new() -> Self { + let ts = Ts { + ctm: Matrix::IDENTITY, + fill: Vec::new(), + font: None, + size: 0.0, + char_spacing: 0.0, + word_spacing: 0.0, + scale: 1.0, + leading: 0.0, + rise: 0.0, + }; + Carry { ts, stack: Vec::new() } + } +} + +fn interpret(doc: &Document, ops: &[Op], fonts_res: &Dict, cache: &mut HashMap, Rc>, carry: &mut Carry) -> Vec { let mut out = Vec::new(); - let mut ts = Ts { - ctm: Matrix::IDENTITY, - fill: Vec::new(), - font: None, - size: 0.0, - char_spacing: 0.0, - word_spacing: 0.0, - scale: 1.0, - leading: 0.0, - rise: 0.0, - }; + let mut ts = carry.ts.clone(); let mut at_bt = (0usize, ts.clone()); - let mut stack: Vec = Vec::new(); + let mut stack: Vec = std::mem::take(&mut carry.stack); let (mut tm, mut tlm) = (Matrix::IDENTITY, Matrix::IDENTITY); let mut bt = 0usize; for (i, op) in ops.iter().enumerate() { @@ -380,6 +395,8 @@ fn interpret(doc: &Document, ops: &[Op], fonts_res: &Dict, cache: &mut HashMap {} } } + carry.ts = ts; + carry.stack = stack; out } @@ -390,9 +407,10 @@ pub fn text_lines(doc: &Document, page: usize) -> Result, EditErro let fonts_res = res.get(b"Font").map(|f| doc.resolve(f)).and_then(|f| f.as_dict().cloned()).unwrap_or_default(); let mut cache = HashMap::new(); let mut lines: Vec = Vec::new(); + let mut carry = Carry::new(); for (si, (_, data)) in content_streams(doc, &p.dict).into_iter().enumerate() { let ops = parse(&data).ops; - let shown = interpret(doc, &ops, &fonts_res, &mut cache); + let shown = interpret(doc, &ops, &fonts_res, &mut cache, &mut carry); let mut last: Option<(usize, f64, f64, f64)> = None; // (bt, baseline, end_x, size) for s in shown { let joins = last.is_some_and(|(bt, base, end, size)| {