From 3e9988954a17cf9474b841e2ec0511b02bf19f53 Mon Sep 17 00:00:00 2001 From: Jayesh-Prajapati9 Date: Fri, 23 Oct 2026 14:15:17 +0530 Subject: [PATCH] M7: CR and CRLF break rows on the styled rewrite path too (#4199) A multi-line or multi-style draft is laid out by rewrite_styled_block, which broke rows only at \n: a CRLF left a stray CR before the break and a lone CR broke nothing. The CR of a CRLF is now skipped and a lone CR breaks the row, keeping the draft's character positions, and a draft whose only break is a lone CR takes the same path as one with \n. --- crates/edit/src/tests.rs | 26 ++++++++++++++++++++++++++ crates/edit/src/text.rs | 3 ++- crates/edit/src/text_regions.rs | 16 +++++++++++++++- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index d614f11b62..918a843daf 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -6717,6 +6717,32 @@ fn a_lone_carriage_return_breaks_an_edited_paragraph() { assert_eq!(lines, ["one", "two"]); } +/// On the styled rewrite path too, `\r\n` is one row break and a lone `\r` another, and no +/// carriage return reaches the page: with no other formatting, and with a part set in red. +#[test] +fn carriage_returns_break_rows_on_the_styled_path() { + let draft = "one\r\ntwo\rthree"; + let red = text::RangeStyle { + range: 0..3, + family: None, + resource: None, + size: None, + color: Some([1.0, 0.0, 0.0]), + underline: None, + char_spacing: None, + scale: None, + }; + for style in [text::BlockStyle::default(), text::BlockStyle { ranges: vec![red], ..Default::default() }] { + let mut doc = text_page("BT /F1 12 Tf 72 700 Td (Hello world) Tj ET"); + text::rewrite_block(&mut doc, 0, 0, Some(draft), &style).unwrap(); + let doc = reopen(&doc); + let content = page_content_bytes(&doc, 0); + assert!(!content.contains(&b'\r') && !content.windows(2).any(|w| w == b"\\r"), "{}", String::from_utf8_lossy(&content)); + let lines: Vec = text::text_lines(&doc, 0).unwrap().into_iter().map(|l| l.text).collect(); + assert_eq!(lines, ["one", "two", "three"], "{style:?}"); + } +} + /// A subset with no space glyph shows each space as a positioning gap one space wide, so a run /// of spaces still takes a run's width (#4199). #[test] diff --git a/crates/edit/src/text.rs b/crates/edit/src/text.rs index 94443ec63d..24cfaab81d 100644 --- a/crates/edit/src/text.rs +++ b/crates/edit/src/text.rs @@ -2726,7 +2726,8 @@ fn rewrite_block_from( && (!style.ranges.is_empty() || style.resource.is_some() || (text.is_some_and(|draft| { - draft.contains('\n') + // A row break, `\n` or a carriage return, takes the path that keeps untouched rows. + draft.contains(['\n', '\r']) || pdfcraft_fonts::source_shaping::required(draft) || !pdfcraft_fonts::embedding::matching_faces(&b.base_font).is_empty() }) || registered_face diff --git a/crates/edit/src/text_regions.rs b/crates/edit/src/text_regions.rs index a775594944..f64483c4d8 100644 --- a/crates/edit/src/text_regions.rs +++ b/crates/edit/src/text_regions.rs @@ -547,11 +547,20 @@ pub(super) fn rewrite_styled_block( let mut programs: HashMap>>> = HashMap::new(); for (range, index) in styled_spans { let source = lines.get(index).unwrap_or(first); - for ch in draft.get(range).unwrap_or_default().chars() { + let start = range.start; + for (offset, ch) in draft.get(range).unwrap_or_default().char_indices() { if ch == '\n' { chars.push((ch, index, source.font.clone())); continue; } + // A carriage return breaks the row too. Before `\n` the pair is one break, so the + // CR stays a `\r` the rows skip. Either way it keeps its place among the draft's + // characters, which rows are counted in. + if ch == '\r' { + let crlf = start.checked_add(offset).and_then(|at| draft.get(at.saturating_add(1)..)).is_some_and(|rest| rest.starts_with('\n')); + chars.push((if crlf { '\r' } else { '\n' }, index, source.font.clone())); + continue; + } let s = ch.to_string(); let name = if let Some(resource) = overrides.get(&index).and_then(|r| r.resource.as_ref()).or(style.resource.as_ref()) { if !metrics.get(resource).is_some_and(|m| source_encodes(m, &s)) { @@ -735,6 +744,11 @@ pub(super) fn rewrite_styled_block( let mut at = 0; while at < chars.len() { let Some(item) = chars.get(at) else { break }; + // The CR of a CRLF: the `\n` after it breaks the row. + if item.0 == '\r' { + at += 1; + continue; + } if item.0 == '\n' { row_ranges.push(row_start..row_start + row.len()); rows.push(std::mem::take(&mut row));