From 81a6ca456cb88d1992dfcaa1bfdbd20e1490d63f Mon Sep 17 00:00:00 2001 From: midasdf <205497066+midasdf@users.noreply.github.com> Date: Fri, 9 Oct 2026 02:29:15 +0900 Subject: [PATCH] M7: preserve ideographic spaces in edited paragraphs (#309) Co-authored-by: Codex --- crates/edit/src/tests.rs | 20 ++++++++++++++++++++ crates/edit/src/text.rs | 28 ++++++++++++++++++++++++---- parity/acrobat-features.toml | 4 ++-- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index a33b1d8f..a48e9647 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -316,6 +316,26 @@ fn japanese_paragraph_uses_unicode_type3_fallback() { assert_eq!(text::text_blocks(&reopened, 0).unwrap()[0].text, replacement); } +#[test] +fn japanese_paragraph_keeps_ideographic_spaces() { + if without_craft_fonts("japanese_paragraph_keeps_ideographic_spaces") { + return; + } + let mut doc = text_page("BT /F1 12 Tf 72 700 Td (Original paragraph) Tj ET"); + let replacement = " 見本商会  御中 "; + let style = text::BlockStyle { width: Some(400.0), ..Default::default() }; + text::rewrite_block(&mut doc, 0, 0, Some(replacement), &style).unwrap(); + let reopened = reopen(&doc); + let lines = text::text_lines(&reopened, 0).unwrap(); + assert_eq!(lines.len(), 1); + assert_eq!(lines[0].text, replacement); + assert_eq!(text::text_blocks(&reopened, 0).unwrap()[0].text, replacement); + // Restyling the saved paragraph without new text must also preserve its spacing. + let mut doc = reopened; + text::rewrite_block(&mut doc, 0, 0, None, &text::BlockStyle { size: Some(14.0), ..style }).unwrap(); + assert_eq!(text::text_lines(&reopen(&doc), 0).unwrap()[0].text, replacement); +} + /// Without craft-fonts there is no Japanese face: editing in Japanese is a clear error that /// leaves the page untouched, never a panic; Latin edits work as before. #[test] diff --git a/crates/edit/src/text.rs b/crates/edit/src/text.rs index b9063351..ff559cf2 100644 --- a/crates/edit/src/text.rs +++ b/crates/edit/src/text.rs @@ -800,13 +800,13 @@ fn group_blocks(lines: &[TextLine]) -> Vec { } else { b.text.push(' '); } - b.text.push_str(l.text.trim()); + b.text.push_str(l.text.trim_matches(paragraph_separator)); b.rect = [b.rect[0].min(l.rect[0]), b.rect[1].min(l.rect[1]), b.rect[2].max(l.rect[2]), b.rect[3].max(l.rect[3])]; b.lines.push(i); } else { gap = None; blocks.push(TextBlock { - text: l.text.trim().to_string(), + text: l.text.trim_matches(paragraph_separator).to_string(), rect: l.rect, base_font: l.base_font.clone(), size: l.size, @@ -820,11 +820,17 @@ fn group_blocks(lines: &[TextLine]) -> Vec { blocks } +// An ideographic space carries intentional Japanese spacing, including paragraph indentation. +// Keep other whitespace normalization unchanged. +fn paragraph_separator(c: char) -> bool { + c.is_whitespace() && c != '\u{3000}' +} + /// Greedy word wrapping to `width` with `advance` giving a string's width. fn wrap(text: &str, width: f64, advance: impl Fn(&str) -> f64) -> Vec { let mut out = Vec::new(); let mut line = String::new(); - for word in text.split_whitespace() { + for word in text.split(paragraph_separator).filter(|word| !word.is_empty()) { let candidate = if line.is_empty() { word.to_string() } else { format!("{line} {word}") }; if !line.is_empty() && advance(&candidate) > width { out.push(std::mem::take(&mut line)); @@ -882,7 +888,7 @@ pub fn rewrite_block(doc: &mut Document, page: usize, block: usize, text: Option let streams = content_streams(doc, &p.dict); let (stream_obj, data) = streams.get(first.stream).cloned().ok_or_else(|| EditError::Invalid("the page's content changed".into()))?; let ops = parse(&data).ops; - let text = text.unwrap_or(&b.text).split_whitespace().collect::>().join(" "); + let text = text.unwrap_or(&b.text).split(paragraph_separator).filter(|word| !word.is_empty()).collect::>().join(" "); let o = &first.origin; let (font_name, old_size) = o.state.font.clone().ok_or_else(|| EditError::Invalid("the paragraph has no font".into()))?; let k = o.k.max(1e-6); @@ -1105,3 +1111,17 @@ pub fn rewrite_block(doc: &mut Document, page: usize, block: usize, text: Option })?; Ok(LineEdit { substituted }) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn paragraph_wrapping_keeps_ideographic_spaces() { + let width = |s: &str| s.chars().count() as f64; + assert_eq!(wrap("A  B C", 4.0, width), ["A  B", "C"]); + assert_eq!(wrap("A\u{a0}B C", 3.0, width), ["A B", "C"]); + assert_eq!(wrap("A\u{2028}B C", 3.0, width), ["A B", "C"]); + assert_eq!(wrap(" \tA B\r\nC ", 3.0, width), ["A B", "C"]); + } +} diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index e8514085..16674649 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -1806,8 +1806,8 @@ milestone = "M7" status = "shipped" commands = ["edit.edit_text"] tools = ["text_edit"] -evidence = ["crates/edit/src/tests.rs::paragraphs_are_found_and_rewrapped", "crates/edit/src/tests.rs::text_lines_are_found_and_replaced_in_place", "crates/ui-egui/tests/editing.rs::editing_existing_text_in_place", "crates/automation/tests/automation.rs::editing_existing_text_through_tools"] -notes = "edited paragraphs are rewrapped to the box width with the font's own advance widths and the original line spacing, starting at the original text matrix; text state and fill colour restored after; a single line grows to the right margin" +evidence = ["crates/edit/src/tests.rs::japanese_paragraph_keeps_ideographic_spaces", "crates/edit/src/tests.rs::paragraphs_are_found_and_rewrapped", "crates/edit/src/tests.rs::text_lines_are_found_and_replaced_in_place", "crates/ui-egui/tests/editing.rs::editing_existing_text_in_place", "crates/automation/tests/automation.rs::editing_existing_text_through_tools"] +notes = "edited paragraphs are rewrapped to the box width with the font's own advance widths and the original line spacing, starting at the original text matrix; text state and fill colour restored after; a single line grows to the right margin; U+3000 ideographic spaces are preserved (including paragraph edges and repeated spaces); Japanese font-style fallback remains limited (#184)" [[feature]] id = "edit.text-move-resize"