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.
This commit is contained in:
Jayesh-Prajapati9
2026-10-23 14:15:17 +05:30
parent d9115bb90b
commit 3e9988954a
3 changed files with 43 additions and 2 deletions
+26
View File
@@ -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<String> = 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]
+2 -1
View File
@@ -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
+15 -1
View File
@@ -547,11 +547,20 @@ pub(super) fn rewrite_styled_block(
let mut programs: HashMap<String, Option<std::sync::Arc<Vec<u8>>>> = 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));