From 0b3b243e9a58ea681930bb8b9d145ed70e27825d Mon Sep 17 00:00:00 2001 From: Lcoding15 <125505497+Lcoding15@users.noreply.github.com> Date: Fri, 23 Oct 2026 21:59:55 +0200 Subject: [PATCH] M7: a bold paragraph's stand-in font no longer turns earlier edits bold (#4280) Edit text set a paragraph whose font lacks a typed character (an accent missing from a subset) in a standard font, always under the resource name PCEdHelv, whatever face it stood for. Rewriting a bold paragraph after a regular one replaced PCEdHelv with Times-Bold, so every paragraph set in the earlier stand-in turned bold, and the editor's draft measurement saw those paragraphs change and grew its frame over them. The stand-in is now named after its face (PCEdTimesBold, ...), as the line and region rewrites already name theirs. Fixes #4280 --- ROADMAP.md | 1 + crates/edit/src/tests.rs | 38 ++++++++++++++++++++++++++++++++++++ crates/edit/src/text.rs | 6 +++--- parity/acrobat-features.toml | 4 ++-- 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 5634d6bf2..fba88c085 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -191,6 +191,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md ## Progress log +- **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. - **2026-10-22 (M4, #3916 partial):** Document edits and Undo/Redo cancel an Organize page drag. Pointer-release regressions check exact page order and save/reopen. - **2026-10-22 (M14, macOS association recovery candidate):** Successful desktop open/save/sign can queue recovery of an existing per-file binding to the running PDFCraft bundle through NSWorkspace. Metadata-only watcher changes retain undo and avoid false overwrite prompts; real same-length/same-mtime edits remain protected. Rebased on 0.12.0 and reuses #3042's shared atomic metadata preservation; the bounded recovery parser does not narrow save limits. Prior native Preview TEST-signature and Finder workflows passed on macOS 27.0.1; other versions remain unqualified. Estimates unchanged. - **2026-10-13 (M7, free image rotation):** A selected page image has a rotate knob (Shift: 15 degree steps) and `image_edit` a `rotate_by` action; the angle is written as one undoable `cm`. Snapping to page edges while moving is in #1740; resize snapping is not done. Estimates unchanged. diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index 0549906db..5d971ca00 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -2414,6 +2414,44 @@ fn missing_glyphs_substitute_helvetica_and_impossible_text_is_refused() { assert!(text::replace_line(&mut doc, 0, 5, "x").is_err(), "no such line"); } +/// #4280: two paragraphs in subsets without Ç or Ã, one regular and one bold. Each edit that adds +/// an accent sets the paragraph in a standard font of its own weight. Editing the bold one must +/// not turn the regular one, set in its stand-in earlier, bold too, and the editor's frame for the +/// bold one must not reach the regular one. +#[test] +fn a_bold_stand_in_font_leaves_an_earlier_regular_one_alone() { + let widths = vec!["600"; 26].join(" "); + let font = |base: &str| { + format!("<< /Type /Font /Subtype /Type1 /BaseFont /{base} /FirstChar 65 /LastChar 90 /Widths [{widths}] /Encoding /WinAnsiEncoding >>") + }; + let (regular, bold) = (font("ABCDEF+Garamond"), font("ABCDEF+Garamond-Bold")); + let content = stream("", "BT /F1 12 Tf 72 700 Td (ACAO) Tj ET BT /F2 12 Tf 300 400 Td (TENSAO) Tj ET"); + let mut doc = build(&[ + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 600 800] /Contents 4 0 R /Resources << /Font << /F1 5 0 R /F2 6 0 R >> >> >>", + &content, + ®ular, + &bold, + ]); + let block = |doc: &Document, text: &str| text::text_blocks(doc, 0).unwrap().iter().position(|b| b.text == text).unwrap(); + let acao = block(&doc, "ACAO"); + assert_eq!(text::replace_block(&mut doc, 0, acao, "AÇÃO").unwrap().substituted.as_deref(), Some("Times-Roman")); + let first = text::text_lines(&doc, 0).unwrap().into_iter().find(|l| l.text == "AÇÃO").unwrap(); + + let tensao = block(&doc, "TENSAO"); + let layout = text::text_edit_layout(&doc, 0, tensao, "TENSÃO", &text::BlockStyle::default()).unwrap(); + let draft = layout.draft_rect.unwrap(); + assert!(draft[3] < first.rect[1] || draft[1] > first.rect[3], "the frame {draft:?} reaches the regular paragraph at {:?}", first.rect); + assert_eq!(text::replace_block(&mut doc, 0, tensao, "TENSÃO").unwrap().substituted.as_deref(), Some("Times-Bold")); + + let doc = reopen(&doc); + let lines = text::text_lines(&doc, 0).unwrap(); + let font_of = |text: &str| lines.iter().find(|l| l.text == text).map(|l| l.base_font.clone()); + assert_eq!(font_of("AÇÃO").as_deref(), Some("Times-Roman"), "{lines:?}"); + assert_eq!(font_of("TENSÃO").as_deref(), Some("Times-Bold"), "{lines:?}"); +} + #[test] fn paragraphs_are_found_and_rewrapped() { let mut doc = text_page( diff --git a/crates/edit/src/text.rs b/crates/edit/src/text.rs index d00393531..5d8f2b10d 100644 --- a/crates/edit/src/text.rs +++ b/crates/edit/src/text.rs @@ -1845,8 +1845,6 @@ fn source_family(base_font: &str) -> crate::added::Family { } } -const SUBSTITUTE_NAME: &str = "PCEdHelv"; -const SUBSTITUTE: &[u8] = SUBSTITUTE_NAME.as_bytes(); const MAX_TYPE3_GLYPHS: usize = 240; /// The generated fonts for one replacement: one per run of at most `max_glyphs` unique characters, @@ -2883,7 +2881,9 @@ fn rewrite_block_from( f.set(b"Subtype".to_vec(), Object::name("Type1")); f.set(b"BaseFont".to_vec(), Object::name(base)); f.set(b"Encoding".to_vec(), Object::name("WinAnsiEncoding")); - let name = if style.family.is_none() { String::from_utf8_lossy(SUBSTITUTE).into_owned() } else { format!("PCEd{}", base.replace('-', "")) }; + // Named after its face, as the line and region rewrites name theirs: one name for every + // stand-in would repoint the paragraphs set in an earlier one at this face (#4280). + let name = format!("PCEd{}", base.replace('-', "")); fonts_res.set(name.clone().into_bytes(), Object::Dict(f)); if style.family.is_none() { substituted = Some(base.to_string()); diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 4ec39848c..881c55da4 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_edit"] -evidence = ["crates/edit/src/tests.rs::missing_glyphs_substitute_helvetica_and_impossible_text_is_refused", "crates/fonts/src/pdf.rs::subset_fonts_only_encode_the_glyphs_they_have", "crates/fonts/src/pdf.rs::composite_fonts_use_to_unicode"] -notes = "the line's font is reused when every character has a code and a glyph (ToUnicode, encodings, Differences; subset glyph checks); otherwise Helvetica (WinAnsi) is used and reported; text no font can show is refused" +evidence = ["crates/edit/src/tests.rs::missing_glyphs_substitute_helvetica_and_impossible_text_is_refused", "crates/edit/src/tests.rs::a_bold_stand_in_font_leaves_an_earlier_regular_one_alone", "crates/fonts/src/pdf.rs::subset_fonts_only_encode_the_glyphs_they_have", "crates/fonts/src/pdf.rs::composite_fonts_use_to_unicode"] +notes = "the line's font is reused when every character has a code and a glyph (ToUnicode, encodings, Differences; subset glyph checks); otherwise Helvetica (WinAnsi) is used and reported; text no font can show is refused. Each standard stand-in has a resource of its own face (PCEdTimesBold, ...), so a bold paragraph's stand-in no longer turns paragraphs set earlier in a regular one bold (#4280); pages already saved with the shared PCEdHelv name by older builds are not repaired" [[feature]] id = "edit.text-format-font"