diff --git a/ROADMAP.md b/ROADMAP.md index 83cfd66532..e8f8c5a69d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -205,7 +205,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-10-24 (M10, #4158 partial):** Word, HTML, RTF and PowerPoint export keep a block that continues another on its line (an italic case name, a link, a superscript) as a run in its own style inside that paragraph, with the source's word space, instead of a paragraph of its own. Style changes that wrap onto the next line still split paragraphs (the reading blocks keep style boundaries). Estimates unchanged. - **2026-10-24 (M3, window move on Wayland, #3970 follow-up):** After a move or resize by PdfCraft's own title bar or edges, the button release given back to egui goes before the pointer leaving the window, so hover no longer sticks at the old place until the pointer moves. Found by CodeRabbit in review of #3970; a unit test covers it. Estimates unchanged. - **2026-10-24 (M14, #4253):** The annotation quick bar remembers Keep tool selected across restarts, including unpinning. Old and malformed settings keep safe defaults; the opt-in UI control channel reads and changes the same preference. Estimates unchanged. -- **2026-10-18 (M7, #2517):** A partial rewrite of the last paragraph closes its marker after that paragraph's last `ET`, so a row left in the source stays in the paragraph. The extra marker is not added across two marked-content regions. Estimates unchanged. +- **2026-10-22 (M7, #2517):** A partial rewrite of the last paragraph closes its marker after that paragraph's last `ET`, so a row left in the source stays in the paragraph. When the next paragraph shares the text object, the marker closes before that neighbour instead of after the shared `ET`. The extra marker is not added across two marked-content regions. Estimates unchanged. - **2026-10-15 (M7, #2517):** A blank line between paragraphs stays a paragraph break. An explicit `TL` is the measured baseline gap. Without one, three or more rows at a repeated step stay one paragraph, and a single wider gap stays a break. Adding a row, even when an earlier row is left in the source, does not absorb the next paragraph on a later edit. Estimates unchanged. - **2026-10-23 (M7, #4280):** Edit text names each standard stand-in font after its face (`PCEdTimesBold`, ...) instead of one shared `PCEdHelv`. Rewriting a bold paragraph whose subset lacks an accent no longer repoints the paragraphs set earlier in a regular stand-in at Times-Bold, and the editor's frame stays on the paragraph being edited. Pages saved by older builds keep their shared name and are not repaired. Estimates unchanged. diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index 81279270a9..c7f4787adc 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -2963,6 +2963,25 @@ fn a_partial_rewrite_that_adds_a_row_keeps_the_neighbour() { } } +/// Both paragraphs share one text object, with a blank line between them. The paragraph +/// marker has to close before the neighbour, not after the shared `ET`. +#[test] +fn a_partial_rewrite_inside_one_text_object_keeps_the_neighbour() { + let src = "BT /F1 12 Tf 14.4 TL 1 0 0 1 72 700 Tm (Alpha first row.) Tj T* (Alpha next row.) Tj T* T* (Neighbor stays.) Tj ET"; + let mut doc = text_page(src); + let texts = |doc: &Document| text::text_blocks(doc, 0).unwrap().into_iter().map(|b| b.text).collect::>(); + assert_eq!(texts(&doc), ["Alpha first row. Alpha next row.", "Neighbor stays."], "the blank line is a paragraph break"); + text::replace_block(&mut doc, 0, 0, "Alpha first row.\nTwo\nThree").unwrap(); + let content = String::from_utf8_lossy(&page_content_bytes(&doc, 0)).into_owned(); + assert_eq!(texts(&doc), ["Alpha first row. Two Three", "Neighbor stays."], "{content}"); + for saved in [reopen(&doc), Document::open(Arc::new(write_full(&doc, &SaveOptions::default()).unwrap())).unwrap()] { + assert_eq!(texts(&saved), ["Alpha first row. Two Three", "Neighbor stays."], "save joined the neighbour"); + let mut again = saved; + text::replace_block(&mut again, 0, 0, "Alpha first row.\nTwo\nFour").unwrap(); + assert_eq!(texts(&reopen(&again)), ["Alpha first row. Two Four", "Neighbor stays."]); + } +} + /// #2516's published steps: adding a row must not make the next edit-by-number rewrite Charlie. /// The issue also asks for a stable paragraph id. That is #2508, and this test does not add one. #[test] diff --git a/crates/edit/src/text_regions/styled.rs b/crates/edit/src/text_regions/styled.rs index 557a86f1d6..5df2d9cd1d 100644 --- a/crates/edit/src/text_regions/styled.rs +++ b/crates/edit/src/text_regions/styled.rs @@ -647,6 +647,10 @@ fn shared_scope(block: &TextBlock, lines: &[TextLine]) -> Option { /// append it after the stream). Closing at the next line's `BT` is too late when that line's /// region opened with `BDC` before the `BT`: the `EMC` would end the neighbour's region. Closing /// inside the rewritten operators leaves a kept source row outside the marker. +/// +/// A neighbour in the same text object has no `BT` of its own. Closing after the shared `ET` +/// would take that neighbour into the marker. The marker then opens after `BT` and closes +/// before the neighbour's text, so `BT` and `ET` stay matched. fn paragraph_marker(block: &TextBlock, lines: &[TextLine], ops: &[Op]) -> Option<(usize, Option, usize)> { let (start, next) = paragraph_span(block, lines)?; if let Some(region) = shared_scope(block, lines) @@ -657,18 +661,32 @@ fn paragraph_marker(block: &TextBlock, lines: &[TextLine], ops: &[Op]) -> Option return None; } } + let belongs: std::collections::HashSet = block.lines.iter().copied().collect(); let end = block.lines.iter().filter_map(|i| lines.get(*i)).flat_map(|line| line.ops.iter().copied().chain(std::iter::once(line.origin.bt_op))).max()?; - let close = ops + let after_text = ops .iter() .enumerate() .skip(end.saturating_add(1)) .find(|(_, op)| op.op.as_slice() == b"ET") .map(|(index, _)| index.saturating_add(1)) .unwrap_or(ops.len()); + let neighbour = lines + .iter() + .enumerate() + .filter(|(index, _)| !belongs.contains(index)) + .filter_map(|(_, line)| line.ops.iter().copied().filter(|op| *op > end).min()) + .min(); + if let Some(op) = neighbour.filter(|op| *op < after_text) { + let open = block.lines.iter().filter_map(|i| lines.get(*i)).flat_map(|line| line.ops.iter().copied()).min()?; + // `BT` stays outside the marker. Both ends are inside the one text object. + if open > start { + return Some((open, None, op)); + } + } // `None` is the next-line index: the close is after this paragraph's `ET`, not at the // following line's `BT`. - Some((start, None, close)) + Some((start, None, after_text)) } struct RewriteAnchor<'a> { diff --git a/docs/gaps.md b/docs/gaps.md index 9457fd7595..4f35e651d1 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -5,6 +5,7 @@ > **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-19 · **Change:** minor (gap 2: composite fonts with `/ToUnicode /Identity-H` read as Unicode, hayro-interpret patch 44; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-17 · **Last updated:** 2026-10-17 · **Change:** minor (gap 6: #2662 redaction audit counts a cover over one word of a long line; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) +> **Last reviewed:** 2026-10-22 · **Last updated:** 2026-10-22 · **Change:** minor (gap 3: #2517 a partial rewrite inside one text object keeps the next paragraph; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-18 · **Change:** minor (gap 3: #2517 a partial rewrite keeps a kept row inside the paragraph and leaves the next marked region closed; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (gap 12: reading order is scored, not just word F1) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (M1: raw-deflate fallback preserves decoding-limit errors; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) @@ -105,9 +106,10 @@ update the numbers. baseline is still replaced. A blank line stays a paragraph break. An explicit `TL` is the measured baseline gap, and growing a paragraph so its new row is one leading above the next one does not absorb that paragraph (#2517). A partial rewrite of the last paragraph keeps a - source row inside that paragraph, and does not close a following marked region early. A heading and smaller body lines (#1756), a - hanging-indent continuation (#2564), and cells in different columns (#1680, #2478) are still - grouped by the same geometry. Estimates are unchanged. + source row inside that paragraph, and does not close a following marked region early. Two + paragraphs that share one text object keep that boundary when the first is rewritten. A heading + and smaller body lines (#1756) and cells in different columns (#1680, #2478) are still grouped + by the same geometry. A row that a list item wraps onto stays with that item. Estimates are unchanged. - **Missing:** robust in-place editing of text PdfCraft didn't write: subset fonts without the needed glyphs, CJK and right-to-left text, rotated text, lists, superscript/subscript, keeping diff --git a/docs/roadmap.md b/docs/roadmap.md index 184a1b7a37..bad3da9a63 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -9,7 +9,7 @@ > **Last reviewed:** 2026-10-23 | **Last updated:** 2026-10-23 | **Change:** minor (M10: table spans retain text from covered slots in Office exports; estimates unchanged) > **Last reviewed:** 2026-10-23 · **Last updated:** 2026-10-23 · **Change:** minor (M10: image export preserves soft-mask Decode opacity ranges; estimates unchanged) > **Last reviewed:** 2026-10-23 · **Last updated:** 2026-10-23 · **Change:** minor (M10: vector printing preserves existing page XObjects when adding annotation appearances; estimates unchanged) -> **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-18 · **Change:** minor (M7 #2517: a partial rewrite keeps its marker around a kept row, and off the next marked region; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) +> **Last reviewed:** 2026-10-22 · **Last updated:** 2026-10-22 · **Change:** minor (M7 #2517: a partial rewrite inside one text object closes its marker before the next paragraph; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M7 #2517: a blank line stays a paragraph break; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-20 | **Last updated:** 2026-10-20 | **Change:** minor (M10, #3532 partial: vector printing retains existing appearances with absent Subtype; estimates unchanged) @@ -266,6 +266,7 @@ Estimates unchanged. | 2026-10-21 | minor | M3: on GNOME/Wayland, a second resize by an edge or move by the tab strip works without a click in between (the compositor kept the button's release from egui). Estimates unchanged. | | 2026-10-22 | minor | M4, #3916 partial: edits and Undo/Redo cancel Organize page drags. Pointer-release and save/reopen regressions check page order. Estimates unchanged. | | 2026-10-22 | minor | M14 #3558: rebased macOS association recovery; reuse #3042 shared metadata preservation instead of a duplicate save wrapper. Estimates unchanged. | +| 2026-10-22 | minor | M7 #2517: a partial rewrite of two paragraphs that share one text object closes the marker before the neighbour, so a later edit does not delete it. Estimates unchanged. | | 2026-10-18 | minor | M7 #2517: a partial rewrite of the last paragraph closes its marker after the last `ET`, and does not close a following marked region early. Estimates unchanged. | | 2026-10-15 | minor | M7 #2517: a blank line between paragraphs stays a paragraph break, and growing the first paragraph does not absorb the next one. Estimates unchanged. | | 2026-10-20 | minor | M4: initial-view window and interface options resolve indirect boolean ViewerPreferences values; synthetic true/false and save/reopen regressions retain defaults for malformed entries. Estimates unchanged. | diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 65a93ebf75..5ff2829e2f 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -1914,8 +1914,8 @@ milestone = "M7" status = "shipped" commands = ["edit.edit_text"] tools = ["text_paragraphs"] -evidence = ["crates/edit/src/tests.rs::paragraphs_are_found_and_rewrapped", "crates/edit/src/tests.rs::a_wrapped_list_item_keeps_its_continuation_and_the_items_after_it", "crates/edit/src/tests.rs::a_blank_line_keeps_the_next_paragraph", "crates/edit/src/tests.rs::a_grown_paragraph_does_not_swallow_the_next_one", "crates/automation/tests/it/automation.rs::a_blank_line_stays_a_separate_paragraph_through_tools", "crates/automation/tests/it/automation.rs::paragraph_grouping_survives_save_and_reopen_through_tools", "crates/edit/src/tests.rs::a_partial_rewrite_of_the_only_paragraph_stays_one_block", "crates/edit/src/tests.rs::a_partial_rewrite_closes_before_the_operator_that_follows_the_paragraph", "crates/edit/src/tests.rs::a_partial_rewrite_of_the_last_marked_paragraph_stays_inside_its_region", "crates/edit/src/tests.rs::a_partial_rewrite_keeps_the_next_marked_region_closed", "crates/edit/src/tests.rs::a_partial_rewrite_of_an_unmarked_paragraph_leaves_the_marked_neighbour_closed", "crates/edit/src/tests.rs::a_partial_rewrite_inside_one_marked_region_leaves_the_neighbour_inside_it", "crates/automation/tests/it/automation.rs::a_partial_rewrite_of_the_last_paragraph_stays_one_through_tools", "crates/edit/src/tests.rs::text_lines_are_found_and_replaced_in_place", "crates/ui-egui/tests/it/editing.rs::editing_existing_text_in_place", "crates/automation/tests/it/automation.rs::editing_existing_text_through_tools"] -notes = "lines grouped into paragraphs by stream, font, size, left alignment and regular baseline spacing; a blank line (about twice the paragraph's own leading) stays a separate paragraph, and editing the first one leaves the next in place (#2517). An explicit TL is that leading. Without one, Td does not set it: three or more rows at one repeated step stay one paragraph, and a single wider gap stays a break. A rewrite marks the whole paragraph, including a row it leaves in the source, so adding a row does not absorb the next paragraph on a later edit. When the paragraph is the last text on the page, that marker closes after the paragraph's last ET, so a kept source row stays inside it. The extra marker is not added when the paragraph is already one marked region and the next line is in a different region. Hyphenated line ends are joined. A row opening with a list marker starts its own item, and the row a list item wraps onto continues it at its hanging indent, so editing one item never takes the items after it. Still open: a larger heading joined to smaller body lines (#1756) and cells in different columns (#1680, #2478). Two rows and no third step still cannot be told from a paragraph break when there is no TL." +evidence = ["crates/edit/src/tests.rs::paragraphs_are_found_and_rewrapped", "crates/edit/src/tests.rs::a_wrapped_list_item_keeps_its_continuation_and_the_items_after_it", "crates/edit/src/tests.rs::a_blank_line_keeps_the_next_paragraph", "crates/edit/src/tests.rs::a_grown_paragraph_does_not_swallow_the_next_one", "crates/automation/tests/it/automation.rs::a_blank_line_stays_a_separate_paragraph_through_tools", "crates/automation/tests/it/automation.rs::paragraph_grouping_survives_save_and_reopen_through_tools", "crates/edit/src/tests.rs::a_partial_rewrite_of_the_only_paragraph_stays_one_block", "crates/edit/src/tests.rs::a_partial_rewrite_closes_before_the_operator_that_follows_the_paragraph", "crates/edit/src/tests.rs::a_partial_rewrite_of_the_last_marked_paragraph_stays_inside_its_region", "crates/edit/src/tests.rs::a_partial_rewrite_keeps_the_next_marked_region_closed", "crates/edit/src/tests.rs::a_partial_rewrite_of_an_unmarked_paragraph_leaves_the_marked_neighbour_closed", "crates/edit/src/tests.rs::a_partial_rewrite_inside_one_marked_region_leaves_the_neighbour_inside_it", "crates/edit/src/tests.rs::a_partial_rewrite_inside_one_text_object_keeps_the_neighbour", "crates/automation/tests/it/automation.rs::a_partial_rewrite_of_the_last_paragraph_stays_one_through_tools", "crates/edit/src/tests.rs::text_lines_are_found_and_replaced_in_place", "crates/ui-egui/tests/it/editing.rs::editing_existing_text_in_place", "crates/automation/tests/it/automation.rs::editing_existing_text_through_tools"] +notes = "lines grouped into paragraphs by stream, font, size, left alignment and regular baseline spacing; a blank line (about twice the paragraph's own leading) stays a separate paragraph, and editing the first one leaves the next in place (#2517). An explicit TL is that leading. Without one, Td does not set it: three or more rows at one repeated step stay one paragraph, and a single wider gap stays a break. A rewrite marks the whole paragraph, including a row it leaves in the source, so adding a row does not absorb the next paragraph on a later edit. When the paragraph is the last text on the page, that marker closes after the paragraph's last ET, so a kept source row stays inside it. When the next paragraph shares that text object, the marker opens after BT and closes before the neighbour's text. The extra marker is not added when the paragraph is already one marked region and the next line is in a different region. Hyphenated line ends are joined. A row opening with a list marker starts its own item, and the row a list item wraps onto continues it at its hanging indent, so editing one item never takes the items after it. Still open: a larger heading joined to smaller body lines (#1756) and cells in different columns (#1680, #2478). Two rows and no third step still cannot be told from a paragraph break when there is no TL." [[feature]] id = "edit.font-reuse-substitution"