diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index 95119c802e..e728a90393 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -4577,6 +4577,8 @@ fn revealed_text_moves_on_top_only_when_painted_over() { fn an_ocr_rewrite_goes_on_top_of_paint_over_where_it_lands() { let scan = "q 200 0 0 100 100 100 cm /Im0 Do Q"; let moved = text::BlockStyle { offset: Some([-290.0, -450.0]), ..Default::default() }; + let sized = text::BlockStyle { size: Some(24.0), ..Default::default() }; + let spaced = text::BlockStyle { line_spacing: Some(3.0), ..Default::default() }; let cases = [ ("BT /F1 12 Tf 3 Tr 400 600 Td (Away) Tj ET", "Moved", &moved), ("BT /F1 12 Tf 3 Tr 400 600 Td (Away) Tj ET", "Moved\nTwice", &moved), @@ -4584,7 +4586,12 @@ fn an_ocr_rewrite_goes_on_top_of_paint_over_where_it_lands() { ("BT /F1 12 Tf 3 Tr 40 150 Td (Hi) Tj ET", "Hello there,\nthis line now runs well past the scan's left edge", &text::BlockStyle::default()), ("BT /F1 12 Tf 3 Tr 110 230 Td (Above) Tj ET", "One\nTwo\nThree\nFour\nFive\nSix", &text::BlockStyle::default()), ]; - for (content, edit, style) in cases { + let low = "q 200 0 0 30 100 300 cm /Im0 Do Q"; + let cases = cases.into_iter().map(|(c, e, s)| (c, e, s, scan)).chain([ + ("BT /F1 12 Tf 3 Tr 110 500 Td (Above) Tj ET", "One\nTwo\nThree\nFour\nFive\nSix", &sized, low), + ("BT /F1 12 Tf 3 Tr 110 500 Td (Above) Tj ET", "One\nTwo\nThree\nFour\nFive\nSix", &spaced, low), + ]); + for (content, edit, style, scan) in cases { let mut doc = image_page_with(&format!("{content} {scan}")); let before = text::text_blocks(&doc, 0).unwrap().remove(0); let ops = pdfcraft_content::parse(&page_content_bytes(&doc, 0)).ops; diff --git a/crates/edit/src/text_regions/styled.rs b/crates/edit/src/text_regions/styled.rs index 2b19a00108..a606952233 100644 --- a/crates/edit/src/text_regions/styled.rs +++ b/crates/edit/src/text_regions/styled.rs @@ -402,6 +402,16 @@ fn paragraph_geometry(doc: &Document, page: &pdfcraft_model::Page, block: &TextB Ok((dx, dy, style.width.unwrap_or(natural).clamp(1.0, 14_400.0))) } +/// How far below a row of `size` the next one is written: the source's leading unless a size +/// or line spacing is chosen. +fn row_step(style: &BlockStyle, size: f64, source_leading: f64) -> f64 { + if style.line_spacing.is_none() && style.size.is_none() { + source_leading + } else { + style.line_spacing.unwrap_or(1.5).clamp(0.5, 5.0) * style.size.unwrap_or(size) + } +} + fn wrap_styled_rows( chars: &[StyledChar], width: f64, @@ -812,8 +822,9 @@ pub(crate) fn rewrite_styled_block( .unwrap_or(first.size * 1.5); // Revealed OCR text that something is painted over goes on top of the page, where it starts // from the initial state. - let largest = style.ranges.iter().filter_map(|r| r.size).chain(style.size).fold(first.size, f64::max); - let pitch = source_leading.max(largest * style.line_spacing.unwrap_or(1.2)); + let largest = + block.lines.iter().filter_map(|i| lines.get(*i)).map(|l| l.size).chain(style.ranges.iter().filter_map(|r| r.size)).fold(first.size, f64::max); + let pitch = source_leading.max(row_step(style, largest, source_leading)); let widest = measured.iter().copied().fold(width, f64::max); // This path lays rows out along user-space baselines, so the paragraph is upright here. let reach = crate::Reach { rect: block.rect, offset: [dx, dy], width: widest, rows: rows.len(), pitch, upright: true }; @@ -972,11 +983,7 @@ pub(crate) fn rewrite_styled_block( x += segment_width; pos = end; } - baseline -= if style.line_spacing.is_none() && style.size.is_none() { - source_leading - } else { - style.line_spacing.unwrap_or(1.5).clamp(0.5, 5.0) * style.size.unwrap_or(anchor.size) - }; + baseline -= row_step(style, anchor.size, source_leading); } block_ops.push(Op::new("Q", vec![])); let on_top = crate::lift_on_top(over, &mut block_ops);