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
This commit is contained in:
Lcoding15
2026-10-23 22:46:56 +02:00
parent 8d99a5ce1f
commit 0b3b243e9a
4 changed files with 44 additions and 5 deletions
+1
View File
@@ -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.
+38
View File
@@ -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,
&regular,
&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(
+3 -3
View File
@@ -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());
+2 -2
View File
@@ -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"